From f3559e9b854b4e339184a4fc46c5e0ac653cf857 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 16 Mar 2024 20:58:07 +0000 Subject: [PATCH 1/6] Initial commit. This adds an initial framework for updated telemetry using .NET metrics. Many of these telemetry points aren't connected, and the old telemetry hasn't been removed. This has also only been applied to the .NET Core project. --- .../Data/ProviderBase/DbConnectionFactory.cs | 27 ++ .../src/Microsoft.Data.SqlClient.csproj | 19 + .../Telemetry/SqlClientMetrics.NetCoreApp.cs | 378 ++++++++++++++++++ .../ProviderBase/DbConnectionPoolGroup.cs | 10 +- .../SqlClient/Telemetry/ComponentTelemetry.cs | 28 ++ .../Telemetry/DbConnectionFactoryTelemetry.cs | 60 +++ .../SqlClient/Telemetry/MetricConstants.cs | 167 ++++++++ .../SqlClient/Telemetry/SqlClientMetrics.cs | 309 ++++++++++++++ .../SqlClient/Telemetry/SqlClientTelemetry.cs | 31 ++ .../Telemetry/TelemetryInterfaces.cs | 196 +++++++++ 10 files changed, 1224 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/ComponentTelemetry.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/DbConnectionFactoryTelemetry.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientTelemetry.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs index df16ee4d23..4c3d3d3218 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs @@ -10,6 +10,7 @@ using Microsoft.Data.Common; using System; using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.Telemetry; namespace Microsoft.Data.ProviderBase { @@ -90,6 +91,32 @@ public void ClearPool(DbConnectionPoolKey key) } } + public Dictionary GetFactoryMetrics() + { + Dictionary connectionPoolGroups = _connectionPoolGroups; + Dictionary telemetry = new(connectionPoolGroups.Count); + + foreach (DbConnectionPoolGroup poolGroup in connectionPoolGroups.Values) + { + // This will ensure that the connection string has does not reveal its credentials + string connectionString = poolGroup.ConnectionOptions.UsersConnectionStringForTrace(); + + // This will return true if the connection string uses integrated authentication and a second connection has + // been opened in an impersonated context. The specification doesn't handle this well, so the only thing to + // do is to roll all of the contexts up into a single connection string. + if (telemetry.ContainsKey(connectionString)) + { + telemetry[connectionString].AddConnectionPool(poolGroup.Telemetry); + } + else + { + telemetry.Add(connectionString, new DbConnectionFactoryTelemetry(poolGroup.Telemetry)); + } + } + + return telemetry; + } + internal virtual DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions) { return null; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index eb15ff1e1a..aa87d9836e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -246,6 +246,24 @@ Microsoft\Data\SqlClient\Server\SqlRecordBuffer.cs + + Microsoft\Data\SqlClient\Telemetry\ComponentTelemetry.cs + + + Microsoft\Data\SqlClient\Telemetry\MetricConstants.cs + + + Microsoft\Data\SqlClient\Telemetry\SqlClientMetrics.cs + + + Microsoft\Data\SqlClient\Telemetry\SqlClientTelemetry.cs + + + Microsoft\Data\SqlClient\Telemetry\DbConnectionFactoryTelemetry.cs + + + Microsoft\Data\SqlClient\Telemetry\TelemetryInterfaces.cs + Microsoft\Data\SqlClient\SqlTransaction.Common.cs @@ -646,6 +664,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs new file mode 100644 index 0000000000..d90487dc27 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs @@ -0,0 +1,378 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Threading; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient.Telemetry +{ + internal sealed partial class SqlClientMetrics + { +#if NETSTANDARD2_0 + private void InitializePlatformSpecificMetrics() { } + + private void IncrementPlatformSpecificMetric(string metricName, in TagList tagList) { } + + private void DecrementPlatformSpecificMetric(string metricName, in TagList tagList) { } + + private void DisposePlatformSpecificMetrics() { } +#else + + private PollingCounter _activeHardConnections; + private IncrementingPollingCounter _hardConnectsPerSecond; + private IncrementingPollingCounter _hardDisconnectsPerSecond; + + private PollingCounter _activeSoftConnections; + private IncrementingPollingCounter _softConnects; + private IncrementingPollingCounter _softDisconnects; + + private PollingCounter _numberOfNonPooledConnections; + private PollingCounter _numberOfPooledConnections; + + private PollingCounter _numberOfActiveConnectionPoolGroups; + private PollingCounter _numberOfInactiveConnectionPoolGroups; + + private PollingCounter _numberOfActiveConnectionPools; + private PollingCounter _numberOfInactiveConnectionPools; + + private PollingCounter _numberOfActiveConnections; + private PollingCounter _numberOfFreeConnections; + private PollingCounter _numberOfStasisConnections; + private IncrementingPollingCounter _numberOfReclaimedConnections; + + private long _activeHardConnectionsCounter = 0; + private long _hardConnectsCounter = 0; + private long _hardDisconnectsCounter = 0; + + private long _activeSoftConnectionsCounter = 0; + private long _softConnectsCounter = 0; + private long _softDisconnectsCounter = 0; + + private long _nonPooledConnectionsCounter = 0; + private long _pooledConnectionsCounter = 0; + + private long _activeConnectionPoolGroupsCounter = 0; + private long _inactiveConnectionPoolGroupsCounter = 0; + + private long _activeConnectionPoolsCounter = 0; + private long _inactiveConnectionPoolsCounter = 0; + + private long _activeConnectionsCounter = 0; + private long _freeConnectionsCounter = 0; + private long _stasisConnectionsCounter = 0; + private long _reclaimedConnectionsCounter = 0; + + // This counter is simply used as a placeholder - GetPlatformSpecificMetric has to return a ref to something. + // It should always be zero. + private long _watchdogCounter = 0; + + private void InitializePlatformSpecificMetrics() + { + _activeHardConnections = new PollingCounter("active-hard-connections", SqlClientEventSource.Log, () => _activeHardConnectionsCounter) + { + DisplayName = "Actual active connections currently made to servers", + DisplayUnits = "count" + }; + + _hardConnectsPerSecond = new IncrementingPollingCounter("hard-connects", SqlClientEventSource.Log, () => _hardConnectsCounter) + { + DisplayName = "Actual connection rate to servers", + DisplayUnits = "count / sec", + DisplayRateTimeScale = TimeSpan.FromSeconds(1) + }; + + _hardDisconnectsPerSecond = new IncrementingPollingCounter("hard-disconnects", SqlClientEventSource.Log, () => _hardDisconnectsCounter) + { + DisplayName = "Actual disconnection rate from servers", + DisplayUnits = "count / sec", + DisplayRateTimeScale = TimeSpan.FromSeconds(1) + }; + + _activeSoftConnections = new PollingCounter("active-soft-connects", SqlClientEventSource.Log, () => _activeSoftConnectionsCounter) + { + DisplayName = "Active connections retrieved from the connection pool", + DisplayUnits = "count" + }; + + _softConnects = new IncrementingPollingCounter("soft-connects", SqlClientEventSource.Log, () => _softConnectsCounter) + { + DisplayName = "Rate of connections retrieved from the connection pool", + DisplayUnits = "count / sec", + DisplayRateTimeScale = TimeSpan.FromSeconds(1) + }; + + _softDisconnects = new IncrementingPollingCounter("soft-disconnects", SqlClientEventSource.Log, () => _softDisconnectsCounter) + { + DisplayName = "Rate of connections returned to the connection pool", + DisplayUnits = "count / sec", + DisplayRateTimeScale = TimeSpan.FromSeconds(1) + }; + + _numberOfNonPooledConnections = new PollingCounter("number-of-non-pooled-connections", SqlClientEventSource.Log, () => _nonPooledConnectionsCounter) + { + DisplayName = "Number of connections not using connection pooling", + DisplayUnits = "count" + }; + + _numberOfPooledConnections = new PollingCounter("number-of-pooled-connections", SqlClientEventSource.Log, () => _pooledConnectionsCounter) + { + DisplayName = "Number of connections managed by the connection pool", + DisplayUnits = "count" + }; + + _numberOfActiveConnectionPoolGroups = new PollingCounter("number-of-active-connection-pool-groups", SqlClientEventSource.Log, () => _activeConnectionPoolGroupsCounter) + { + DisplayName = "Number of active unique connection strings", + DisplayUnits = "count" + }; + + _numberOfInactiveConnectionPoolGroups = new PollingCounter("number-of-inactive-connection-pool-groups", SqlClientEventSource.Log, () => _inactiveConnectionPoolGroupsCounter) + { + DisplayName = "Number of unique connection strings waiting for pruning", + DisplayUnits = "count" + }; + + _numberOfActiveConnectionPools = new PollingCounter("number-of-active-connection-pools", SqlClientEventSource.Log, () => _activeConnectionPoolsCounter) + { + DisplayName = "Number of active connection pools", + DisplayUnits = "count" + }; + + _numberOfInactiveConnectionPools = new PollingCounter("number-of-inactive-connection-pools", SqlClientEventSource.Log, () => _inactiveConnectionPoolsCounter) + { + DisplayName = "Number of inactive connection pools", + DisplayUnits = "count" + }; + + _numberOfActiveConnections = new PollingCounter("number-of-active-connections", SqlClientEventSource.Log, () => _activeConnectionsCounter) + { + DisplayName = "Number of active connections", + DisplayUnits = "count" + }; + + _numberOfFreeConnections = new PollingCounter("number-of-free-connections", SqlClientEventSource.Log, () => _freeConnectionsCounter) + { + DisplayName = "Number of ready connections in the connection pool", + DisplayUnits = "count" + }; + + _numberOfStasisConnections = new PollingCounter("number-of-stasis-connections", SqlClientEventSource.Log, () => _stasisConnectionsCounter) + { + DisplayName = "Number of connections currently waiting to be ready", + DisplayUnits = "count" + }; + + _numberOfReclaimedConnections = new IncrementingPollingCounter("number-of-reclaimed-connections", SqlClientEventSource.Log, () => _reclaimedConnectionsCounter) + { + DisplayName = "Number of reclaimed connections from GC", + DisplayUnits = "count", + DisplayRateTimeScale = TimeSpan.FromSeconds(1) + }; + } + + private KeyValuePair GetTagByName(string tagName, int likelyIndex, in TagList tagList) + { + KeyValuePair tagValue; + + // We have control over the initial tag list, so in almost every circumstance this shortcut will be used. + // It spares us from a loop, however small. + // In most cases, index 0 is the connection pool name. + if (likelyIndex > 0 && likelyIndex < tagList.Count) + { + tagValue = tagList[likelyIndex]; + + if (string.Equals(tagValue.Key, tagName, StringComparison.OrdinalIgnoreCase)) + { + return tagValue; + } + } + + for(int i = 0; i < tagList.Count; i++) + { + tagValue = tagList[i]; + + if (string.Equals(tagValue.Key, tagName, StringComparison.OrdinalIgnoreCase)) + { + return tagValue; + } + } + + throw ADP.CollectionIndexString(typeof(KeyValuePair), nameof(KeyValuePair.Key), tagName, typeof(TagList)); + } + + private ref long GetPlatformSpecificMetric(string metricName, in TagList tagList, out bool successful) + { + KeyValuePair associatedTag; + + successful = true; + + switch (metricName) + { + case MetricNames.Connections.Usage: + associatedTag = GetTagByName(MetricAttributes.State, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + { + return ref _activeConnectionsCounter; + } + else if(string.Equals((string)associatedTag.Value, MetricAttributeValues.IdleState, StringComparison.OrdinalIgnoreCase)) + { + return ref _freeConnectionsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.StasisState, StringComparison.OrdinalIgnoreCase)) + { + return ref _stasisConnectionsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ReclaimedState, StringComparison.OrdinalIgnoreCase)) + { + return ref _reclaimedConnectionsCounter; + } + break; + case MetricNames.ConnectionPoolGroups.Usage: + associatedTag = GetTagByName(MetricAttributes.State, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + { + return ref _activeConnectionPoolGroupsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.IdleState, StringComparison.OrdinalIgnoreCase)) + { + return ref _inactiveConnectionPoolGroupsCounter; + } + break; + case MetricNames.ConnectionPools.Usage: + associatedTag = GetTagByName(MetricAttributes.State, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + { + return ref _activeConnectionPoolsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.IdleState, StringComparison.OrdinalIgnoreCase)) + { + return ref _inactiveConnectionPoolsCounter; + } + break; + case MetricNames.Connections.HardUsage: + associatedTag = GetTagByName(MetricAttributes.Type, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricAttributeValues.PooledConnectionType, StringComparison.OrdinalIgnoreCase)) + { + return ref _pooledConnectionsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.NonPooledConnectionType, StringComparison.OrdinalIgnoreCase)) + { + return ref _nonPooledConnectionsCounter; + } + break; + case MetricNames.Connections.Connects: + associatedTag = GetTagByName(MetricAttributes.Type, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricAttributeValues.HardActionType, StringComparison.OrdinalIgnoreCase)) + { + return ref _hardConnectsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.SoftActionType, StringComparison.OrdinalIgnoreCase)) + { + return ref _softConnectsCounter; + } + break; + case MetricNames.Connections.Disconnects: + associatedTag = GetTagByName(MetricAttributes.Type, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricAttributeValues.HardActionType, StringComparison.OrdinalIgnoreCase)) + { + return ref _hardDisconnectsCounter; + } + else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.SoftActionType, StringComparison.OrdinalIgnoreCase)) + { + return ref _softDisconnectsCounter; + } + break; + case MetricNames.Connections.HardTotal: + return ref _activeHardConnectionsCounter; + case MetricNames.Connections.SoftTotal: + return ref _activeSoftConnectionsCounter; + } + + successful = false; + return ref _watchdogCounter; + } + + private void IncrementPlatformSpecificMetric(string metricName, in TagList tagList) + { + ref long counterPointer = ref GetPlatformSpecificMetric(metricName, in tagList, out bool successful); + + if (successful) + { + Interlocked.Increment(ref counterPointer); + } + } + + private void DecrementPlatformSpecificMetric(string metricName, in TagList tagList) + { + ref long counterPointer = ref GetPlatformSpecificMetric(metricName, in tagList, out bool successful); + + if (successful) + { + Interlocked.Decrement(ref counterPointer); + } + } + + private void DisposePlatformSpecificMetrics() + { + _activeHardConnections?.Dispose(); + _activeHardConnections = null; + + _hardConnectsPerSecond?.Dispose(); + _hardConnectsPerSecond = null; + + _hardDisconnectsPerSecond?.Dispose(); + _hardDisconnectsPerSecond = null; + + _activeSoftConnections?.Dispose(); + _activeSoftConnections = null; + + _softConnects?.Dispose(); + _softConnects = null; + + _softDisconnects?.Dispose(); + _softDisconnects = null; + + _numberOfNonPooledConnections?.Dispose(); + _numberOfNonPooledConnections = null; + + _numberOfPooledConnections?.Dispose(); + _numberOfPooledConnections = null; + + _numberOfActiveConnectionPoolGroups?.Dispose(); + _numberOfActiveConnectionPoolGroups = null; + + _numberOfInactiveConnectionPoolGroups?.Dispose(); + _numberOfInactiveConnectionPoolGroups = null; + + _numberOfActiveConnectionPools?.Dispose(); + _numberOfActiveConnectionPools = null; + + _numberOfInactiveConnectionPools?.Dispose(); + _numberOfInactiveConnectionPools = null; + + _numberOfActiveConnections?.Dispose(); + _numberOfActiveConnections = null; + + _numberOfFreeConnections?.Dispose(); + _numberOfFreeConnections = null; + + _numberOfStasisConnections?.Dispose(); + _numberOfStasisConnections = null; + + _numberOfReclaimedConnections?.Dispose(); + _numberOfReclaimedConnections = null; + } +#endif + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionPoolGroup.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionPoolGroup.cs index 7568340594..e59b6f92e1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionPoolGroup.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionPoolGroup.cs @@ -34,6 +34,7 @@ sealed internal class DbConnectionPoolGroup private readonly DbConnectionOptions _connectionOptions; private readonly DbConnectionPoolKey _poolKey; private readonly DbConnectionPoolGroupOptions _poolGroupOptions; + private readonly SqlClient.Telemetry.DbConnectionPoolTelemetry _telemetry; private ConcurrentDictionary _poolCollection; private int _state; // see PoolGroupState* below @@ -66,6 +67,8 @@ internal DbConnectionPoolGroup(DbConnectionOptions connectionOptions, DbConnecti // we check _poolGroupOptions first _poolCollection = new ConcurrentDictionary(); _state = PoolGroupStateActive; + // Keep and maintain a single instance of the current telemetry + _telemetry = new SqlClient.Telemetry.DbConnectionPoolTelemetry(poolGroupOptions); } internal DbConnectionOptions ConnectionOptions => _connectionOptions; @@ -107,6 +110,8 @@ internal DbMetaDataFactory MetaDataFactory } } + internal SqlClient.Telemetry.DbConnectionPoolTelemetry Telemetry => _telemetry; + internal int Clear() { // must be multi-thread safe with competing calls by Clear and Prune via background thread @@ -123,7 +128,7 @@ internal int Clear() } } - // Then, if a new collection was created, release the pools from the old collection + // Then, if a new collection was created, release the pools from the old collection and reset telemetry to zero if (oldPoolCollection != null) { foreach (KeyValuePair entry in oldPoolCollection) @@ -138,6 +143,7 @@ internal int Clear() connectionFactory.QueuePoolForRelease(pool, true); } } + _telemetry.Reset(); } // Finally, return the pool collection count - this may be non-zero if something was added while we were clearing @@ -198,6 +204,7 @@ internal DbConnectionPool GetConnectionPool(DbConnectionFactory connectionFactor bool addResult = _poolCollection.TryAdd(currentIdentity, newPool); Debug.Assert(addResult, "No other pool with current identity should exist at this point"); SqlClientEventSource.Log.EnterActiveConnectionPool(); + _telemetry.ReportNewPool(); #if NETFRAMEWORK connectionFactory.PerformanceCounters.NumberOfActiveConnectionPools.Increment(); #endif @@ -279,6 +286,7 @@ internal bool Prune() #if NETFRAMEWORK connectionFactory.PerformanceCounters.NumberOfActiveConnectionPools.Decrement(); #endif + _telemetry.ReportRemovedPool(); connectionFactory.QueuePoolForRelease(pool, false); } else diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/ComponentTelemetry.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/ComponentTelemetry.cs new file mode 100644 index 0000000000..9eed1a1e18 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/ComponentTelemetry.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Telemetry +{ + internal class ComponentTelemetry + { + public SqlClientEventSource Tracing { get; } + + public ComponentTelemetry(SqlClientEventSource tracing) + { + Tracing = tracing; + } + } + + internal sealed class ComponentTelemetry : ComponentTelemetry + where TMetrics : class, IMetrics + { + public TMetrics Metrics { get; } + + public ComponentTelemetry(SqlClientEventSource tracing, TMetrics metrics) + : base(tracing) + { + Metrics = metrics; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/DbConnectionFactoryTelemetry.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/DbConnectionFactoryTelemetry.cs new file mode 100644 index 0000000000..21a7f98198 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/DbConnectionFactoryTelemetry.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Threading; +using Microsoft.Data.ProviderBase; + +namespace Microsoft.Data.SqlClient.Telemetry +{ + // Stores telemetry for a single DbConnectionPoolGroup. Maintained by the connection pool group itself + internal sealed class DbConnectionPoolTelemetry + { + private readonly DbConnectionPoolGroupOptions _options; + private long _poolCount; + + public long TotalMaxPoolSize => _poolCount * (long)_options.MaxPoolSize; + + public long TotalMinPoolSize => _poolCount * (long)_options.MinPoolSize; + + public DbConnectionPoolTelemetry(DbConnectionPoolGroupOptions poolGroupOptions) + { + _options = poolGroupOptions; + } + + public void ReportNewPool() + { + Interlocked.Increment(ref _poolCount); + } + + public void ReportRemovedPool() + { + Interlocked.Decrement(ref _poolCount); + } + + public void Reset() + { + Interlocked.Exchange(ref _poolCount, 0); + } + } + + internal struct DbConnectionFactoryTelemetry + { + public long TotalMinPoolSize { get; private set; } + + public long TotalMaxPoolSize { get; private set; } + + public DbConnectionFactoryTelemetry(DbConnectionPoolTelemetry telemetry) + { + TotalMinPoolSize = telemetry.TotalMinPoolSize; + TotalMaxPoolSize = telemetry.TotalMaxPoolSize; + } + + public void AddConnectionPool(DbConnectionPoolTelemetry telemetry) + { + TotalMinPoolSize += telemetry.TotalMinPoolSize; + TotalMaxPoolSize += telemetry.TotalMaxPoolSize; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs new file mode 100644 index 0000000000..6ed648d141 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Telemetry +{ + internal static class MetricNames + { + // Metric names prefixed with "db.client." are documented at + // https://opentelemetry.io/docs/specs/semconv/database/database-metrics/ + private const string StandardsPrefix = "db.client."; + private const string LibrarySpecificPrefix = "sqlclient.db.client."; + + public static class Connections + { + private const string StandardsPrefix = MetricNames.StandardsPrefix + "connections."; + private const string LibrarySpecificPrefix = MetricNames.LibrarySpecificPrefix + "connections."; + + public const string Usage = StandardsPrefix + "usage"; + + public const string MaxIdle = StandardsPrefix + "idle.max"; + + public const string MinIdle = StandardsPrefix + "idle.min"; + + public const string Max = StandardsPrefix + "max"; + + public const string PendingRequests = StandardsPrefix + "pending_requests"; + + public const string Timeouts = StandardsPrefix + "timeouts"; + + public const string CreationTime = StandardsPrefix + "create_time"; + + public const string WaitTime = StandardsPrefix + "wait_time"; + + public const string UsageTime = StandardsPrefix + "use_time"; + + public const string HardUsage = LibrarySpecificPrefix + "hard.usage"; + + public const string Connects = LibrarySpecificPrefix + "connects"; + + public const string Disconnects = LibrarySpecificPrefix + "disconnects"; + + public const string ServerCommandTime = LibrarySpecificPrefix + "server_command_time"; + + public const string NetworkWaitTime = LibrarySpecificPrefix + "network_wait_time"; + + public const string ByteIo = LibrarySpecificPrefix + "io"; + + public const string BuffersIo = LibrarySpecificPrefix + "buffers"; + + public const string HardTotal = LibrarySpecificPrefix + "hard.total"; + + public const string SoftTotal = LibrarySpecificPrefix + "soft.total"; + } + + public static class ConnectionPools + { + private const string LibrarySpecificPrefix = MetricNames.LibrarySpecificPrefix + "connection_pools."; + + public const string Usage = LibrarySpecificPrefix + "usage"; + } + + public static class ConnectionPoolGroups + { + private const string LibrarySpecificPrefix = MetricNames.LibrarySpecificPrefix + "connection_pool_groups."; + + public const string Usage = LibrarySpecificPrefix + "usage"; + } + + public static class Commands + { + private const string LibrarySpecificPrefix = MetricNames.LibrarySpecificPrefix + "commands."; + + public const string Failed = LibrarySpecificPrefix + ".failed"; + + public const string UsageTime = LibrarySpecificPrefix + "use_time"; + + public const string PreparedRatio = LibrarySpecificPrefix + "prepared_ratio"; + + public const string Total = LibrarySpecificPrefix + "total"; + } + + public static class Transactions + { + private const string LibrarySpecificPrefix = MetricNames.LibrarySpecificPrefix + "transactions."; + + public const string Committed = LibrarySpecificPrefix + ".committed"; + + public const string Active = LibrarySpecificPrefix + ".active"; + + public const string RolledBack = LibrarySpecificPrefix + ".rolled_back"; + + public const string Total = LibrarySpecificPrefix + "total"; + + public const string CommitTime = LibrarySpecificPrefix + "commit_time"; + + public const string RollbackTime = LibrarySpecificPrefix + "rollback_time"; + + public const string ActiveTime = LibrarySpecificPrefix + "active_time"; + } + } + + internal static class MetricAttributes + { + public const string PoolName = "pool.name"; + + public const string State = "state"; + + public const string Direction = "direction"; + + public const string Type = "type"; + } + + internal static class MetricAttributeValues + { + public const string IdleState = "idle"; + + public const string ActiveState = "active"; + + public const string StasisState = "stasis"; + + public const string ReclaimedState = "reclaimed"; + + public const string TransmitDirection = "transmit"; + + public const string ReceiveDirection = "receive"; + + public const string PooledConnectionType = "pooled"; + + public const string NonPooledConnectionType = "unpooled"; + + public const string HardConnectionType = "hard"; + + public const string SoftConnectionType = "soft"; + + public const string HardActionType = "hard"; + + public const string SoftActionType = "soft"; + } + + internal static class MetricUnits + { + public const string Connection = "{connection}"; + + public const string ConnectionPool = "{connection_pool}"; + + public const string ConnectionPoolGroup = "{connection_pool_group}"; + + public const string Command = "{command}"; + + public const string Transaction = "{transaction}"; + + public const string Request = "{request}"; + + public const string Timeout = "{timeout}"; + + public const string Connect = "{connect}"; + + public const string Disconnect = "{disconnect}"; + + public const string Buffer = "{buffer}"; + + public const string Millisecond = "ms"; + + public const string Byte = "By"; + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs new file mode 100644 index 0000000000..1ea7d0ff07 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Numerics; + +namespace Microsoft.Data.SqlClient.Telemetry +{ + internal sealed partial class SqlClientMetrics : IDisposable, IConnectionMetrics, ICommandMetrics, ITransactionMetrics + { + // This version is separate to the assembly version, following semantic versioning. + public const string MeteringVersion = "0.1.0"; + + private bool _disposedValue; + + private readonly bool _enablePlatformSpecificMetrics; + + private readonly Meter _meter; + /// + /// Standard: db.client.connections.usage{pool.name="", state="active|idle"} + /// Extended: db.client.connections.usage{pool.name="", state="stasis"} + /// Extended: db.client.connections.usage{pool.name="", type="hard|soft"} + /// + private readonly Counter _connectionUsageCounter; + /// + /// Standard: db.client.connections.idle.max{pool.name=""} + /// + private readonly ObservableCounter _connectionMaxIdleCounter; + /// + /// Standard: db.client.connections.idle.min{pool.name=""} + /// + private readonly ObservableCounter _connectionMinIdleCounter; + /// + /// Standard: db.client.connections.max{pool.name=""} + /// + private readonly ObservableCounter _connectionMaxCounter; + /// + /// Standard: db.client.connections.pending_requests{pool.name=""} + /// + private readonly Counter _connectionPendingRequestsCounter; + /// + /// Standard: db.client.connections.timeouts{pool.name=""} + /// + private readonly Counter _connectionTimeoutsCounter; + /// + /// Standard: db.client.connections.create_time{pool.name=""} + /// + private readonly Histogram _connectionCreationTimeHistogram; + /// + /// Standard: db.client.connections.wait_time{pool.name=""} + /// + private readonly Histogram _connectionWaitTimeHistogram; + /// + /// Standard: db.client.connections.use_time{pool.name=""} + /// + private readonly Histogram _connectionUsageTimeHistogram; + /// + /// Extended: sqlclient.db.client.connection_pools.usage{pool.name="", state="active|idle"} + /// + private readonly Counter _connectionPoolUsageCounter; + /// + /// Extended: sqlclient.db.client.connection_pool_groups.usage{pool.name="", state="active|idle"} + /// + private readonly Counter _connectionPoolGroupUsageCounter; + /// + /// Extended: sqlclient.db.client.connections.hard.usage{pool.name="", type="pooled|unpooled"} + /// + private readonly Counter _connectionHardUsageCounter; + /// + /// Extended: sqlclient.db.client.connections.connects{pool.name="", type="hard|soft"} + /// + private readonly Counter _connectionConnectCounter; + /// + /// Extended: sqlclient.db.client.connections.disconnects{pool.name="", type="hard|soft"} + /// + private readonly Counter _connectionDisconnectCounter; + + public SqlClientMetrics(bool enablePlatformSpecificMetrics) + { + _enablePlatformSpecificMetrics = enablePlatformSpecificMetrics; + + _meter = new Meter("Microsoft.Data.SqlClient", MeteringVersion); + _connectionUsageCounter = _meter.CreateCounter(MetricNames.Connections.Usage, MetricUnits.Connection); + _connectionMaxIdleCounter = _meter.CreateObservableCounter(MetricNames.Connections.MaxIdle, GetMaxConnectionPoolSizes, MetricUnits.Connection); + _connectionMinIdleCounter = _meter.CreateObservableCounter(MetricNames.Connections.MinIdle, GetMinConnectionPoolSizes, MetricUnits.Connection); + _connectionMaxCounter = _meter.CreateObservableCounter(MetricNames.Connections.Max, GetMaxConnectionPoolSizes, MetricUnits.Connection); + + // The five metrics below are new, and have not yet been integrated. + + // This must be reported from a SqlCommand. The connection string will need to come from Command.Connection.ConnectionOptions.UsersConnectionStringForTrace + // A command begins a pending request when it starts. It leaves the pending request when one of the four states below occurs: + // 1. It completes successfully + // 2. It completes, having encountered server-side errors + // 3. It times out + // 4. Its underlying connection times out/is broken + // It does not leave and re-enter the pending state when it's being retried. + _connectionPendingRequestsCounter = _meter.CreateCounter(MetricNames.Connections.PendingRequests, MetricUnits.Request); + + // This is to be reported from TdsParserStateObject.OnTimeoutCore for both .NET Core and Framework. The connection string will come from + // _parser.Connection.ConnectionOptions.UsersConnectionStringForTrace. + // Besides OnTimeoutCore, two other places add a SqlError with an error core of TdsEnums.TIMEOUT_EXPIRED. These are: + // 1. TdsParserStateObject.ReadSniError + // 2. TdsParserStateObject.WriteSni + // 3. TdsParserStateObject.CheckResetConnection + // If they are only generated when a connection timeout (rather than a command timeout) occurs, they will also report a timeout. + // The client's connection resiliency functionality could result in multiple connection timeouts, but still result in an open connection. + _connectionTimeoutsCounter = _meter.CreateCounter(MetricNames.Connections.Timeouts, MetricUnits.Timeout); + + // This creation time refers specifically refers to the time taken to open a new hard connection. It is to be reported as the time + // taken to execute the body of SqlConnectionFactory.CreateConnection. + _connectionCreationTimeHistogram = _meter.CreateHistogram(MetricNames.Connections.CreationTime, MetricUnits.Millisecond); + + // The connection wait time refers to the time taken to obtain an open connection from the connection pool. It thus encompasses the creation time. + // It's reported from SqlConnection.Open, but the data reported by this metric (and the one below it) is also reported by SqlStatistics. + // With that in mind, SqlStatistics will now always be collected. SqlConnection.StatisticsEnabled will control whether or not GetStatistics + // returns these values. Setting its value to false will set the connection's closed timestamp in the statistics, (if the connection is open) + // and setting its value to true will set the connection's open timestamp (if the connection is open) + _connectionWaitTimeHistogram = _meter.CreateHistogram(MetricNames.Connections.WaitTime, MetricUnits.Millisecond); + + // This is the time difference between the connection's open and close timestamps. It is not affected by disabling and enabling SqlStatistics. + _connectionUsageTimeHistogram = _meter.CreateHistogram(MetricNames.Connections.UsageTime, MetricUnits.Millisecond); + + _connectionPoolUsageCounter = _meter.CreateCounter(MetricNames.ConnectionPools.Usage, MetricUnits.ConnectionPool); + _connectionPoolGroupUsageCounter = _meter.CreateCounter(MetricNames.ConnectionPoolGroups.Usage, MetricUnits.ConnectionPoolGroup); + _connectionHardUsageCounter = _meter.CreateCounter(MetricNames.Connections.HardUsage, MetricUnits.Connection); + _connectionConnectCounter = _meter.CreateCounter(MetricNames.Connections.Connects, MetricUnits.Connect); + _connectionDisconnectCounter = _meter.CreateCounter(MetricNames.Connections.Disconnects, MetricUnits.Disconnect); + + if (_enablePlatformSpecificMetrics) + { + InitializePlatformSpecificMetrics(); + } + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if (_enablePlatformSpecificMetrics) + { + DisposePlatformSpecificMetrics(); + } + + _meter.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // This method is designed to handle backwards compatibility with the older performance counters (.NET Framework) and event counters. + // Since they're all counters, we only need to implement the compatibility layer here. + private void WriteInstrumentValue(Counter counter, long value, in TagList tagList) + { + counter.Add(value, in tagList); + if (_enablePlatformSpecificMetrics) + { + if (value < 0) + { + DecrementPlatformSpecificMetric(counter.Name, in tagList); + } + else if (value > 0) + { + IncrementPlatformSpecificMetric(counter.Name, in tagList); + } + } + } + + #region IConnectionMetrics implementation + + void IConnectionMetrics.HardConnectRequest(ConnectionMetricTagListCollection tagList) + { + WriteInstrumentValue(_connectionUsageCounter, 1, in tagList.HardConnectionsTags); + WriteInstrumentValue(_connectionConnectCounter, 1, in tagList.HardConnectsTags); + } + + void IConnectionMetrics.HardDisconnectRequest(ConnectionMetricTagListCollection tagList) + { + WriteInstrumentValue(_connectionUsageCounter, -1, in tagList.HardConnectionsTags); + WriteInstrumentValue(_connectionDisconnectCounter, 1, in tagList.HardDisconnectsTags); + } + + void IConnectionMetrics.SoftConnectRequest(ConnectionMetricTagListCollection tagList) + { + WriteInstrumentValue(_connectionUsageCounter, 1, in tagList.SoftConnectionsTags); + WriteInstrumentValue(_connectionConnectCounter, 1, in tagList.SoftConnectsTags); + } + + void IConnectionMetrics.SoftDisconnectRequest(ConnectionMetricTagListCollection tagList) + { + WriteInstrumentValue(_connectionUsageCounter, -1, in tagList.SoftConnectionsTags); + WriteInstrumentValue(_connectionDisconnectCounter, 1, in tagList.SoftDisconnectsTags); + } + + void IConnectionMetrics.Timeout(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionTimeoutsCounter, 1, in tagList.TimeoutsTags); + + void IConnectionMetrics.ConnectionCreationTime(in TimeSpan creationTime, ConnectionMetricTagListCollection tagList) + => _connectionCreationTimeHistogram.Record(creationTime.TotalMilliseconds, in tagList.ConnectionCreationTimeTags); + + void IConnectionMetrics.ConnectionWaitTime(in TimeSpan waitTime, ConnectionMetricTagListCollection tagList) + => _connectionWaitTimeHistogram.Record(waitTime.TotalMilliseconds, in tagList.ConnectionWaitTimeTags); + + void IConnectionMetrics.ConnectionUsageTime(in TimeSpan usageTime, ConnectionMetricTagListCollection tagList) + => _connectionUsageTimeHistogram.Record(usageTime.TotalMilliseconds, in tagList.ConnectionUsageTimeTags); + + void IConnectionMetrics.EnterPendingRequest(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPendingRequestsCounter, 1, in tagList.PendingRequestTags); + + void IConnectionMetrics.ExitPendingRequest(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPendingRequestsCounter, -1, in tagList.PendingRequestTags); + + void IConnectionMetrics.EnterNonPooledConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionHardUsageCounter, 1, in tagList.NonPooledHardConnectionUsageTags); + + void IConnectionMetrics.ExitNonPooledConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionHardUsageCounter, -1, in tagList.NonPooledHardConnectionUsageTags); + + void IConnectionMetrics.EnterPooledConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionHardUsageCounter, 1, in tagList.PooledHardConnectionUsageTags); + + void IConnectionMetrics.ExitPooledConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionHardUsageCounter, -1, in tagList.PooledHardConnectionUsageTags); + + void IConnectionMetrics.EnterActiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolGroupUsageCounter, 1, in tagList.ActiveConnectionPoolGroupsTags); + + void IConnectionMetrics.ExitActiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolGroupUsageCounter, -1, in tagList.ActiveConnectionPoolGroupsTags); + + void IConnectionMetrics.EnterInactiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolGroupUsageCounter, 1, in tagList.IdleConnectionPoolGroupsTags); + + void IConnectionMetrics.ExitInactiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolGroupUsageCounter, -1, in tagList.IdleConnectionPoolGroupsTags); + + void IConnectionMetrics.EnterActiveConnectionPool(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolUsageCounter, 1, in tagList.ActiveConnectionPoolsTags); + + void IConnectionMetrics.ExitActiveConnectionPool(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolUsageCounter, -1, in tagList.ActiveConnectionPoolsTags); + + void IConnectionMetrics.EnterInactiveConnectionPool(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolUsageCounter, 1, in tagList.IdleConnectionPoolsTags); + + void IConnectionMetrics.ExitInactiveConnectionPool(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionPoolUsageCounter, -1, in tagList.IdleConnectionPoolsTags); + + void IConnectionMetrics.EnterActiveConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, 1, in tagList.ActiveConnectionsTags); + + void IConnectionMetrics.ExitActiveConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, -1, in tagList.ActiveConnectionsTags); + + void IConnectionMetrics.EnterFreeConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, 1, in tagList.IdleConnectionsTags); + + void IConnectionMetrics.ExitFreeConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, -1, in tagList.IdleConnectionsTags); + + void IConnectionMetrics.EnterStasisConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, 1, in tagList.StasisConnectionsTags); + + void IConnectionMetrics.ExitStasisConnection(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, -1, in tagList.StasisConnectionsTags); + + void IConnectionMetrics.ReclaimedConnectionRequest(ConnectionMetricTagListCollection tagList) + => WriteInstrumentValue(_connectionUsageCounter, 1, in tagList.ReclaimedConnectionsTags); + #endregion + + #region Observable metrics implementation + // TotalMinPoolSize and TotalMaxPoolSize are grouped by connection string, and thus come from DbConnectionPoolGroup. This can result in inaccurate metrics + // if Windows (or Kerberos, or a future authentication mechanism which permits process-level impersonation) authentication is used. A client could connect + // to one database using one connection string, impersonate a different Windows/Kerberos identity at the process level, then use the same connection string + // to connect to a different database. In this situation, both connection pools will be rolled up into a single metric. + private IEnumerable> GetMinConnectionPoolSizes() + { + Dictionary connectionPoolMetrics = SqlConnectionFactory.SingletonInstance.GetFactoryMetrics(); + + foreach (KeyValuePair poolMetric in connectionPoolMetrics) + yield return new Measurement(poolMetric.Value.TotalMinPoolSize, + new KeyValuePair(MetricAttributes.PoolName, poolMetric.Key)); + } + + private IEnumerable> GetMaxConnectionPoolSizes() + { + Dictionary connectionPoolMetrics = SqlConnectionFactory.SingletonInstance.GetFactoryMetrics(); + + foreach (KeyValuePair poolMetric in connectionPoolMetrics) + yield return new Measurement(poolMetric.Value.TotalMaxPoolSize, + new KeyValuePair(MetricAttributes.PoolName, poolMetric.Key)); + } + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientTelemetry.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientTelemetry.cs new file mode 100644 index 0000000000..93b0f6f526 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientTelemetry.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Telemetry +{ + internal sealed class SqlClientTelemetry + { + private static SqlClientTelemetry s_instance; + + public static SqlClientTelemetry Instance => s_instance ??= new SqlClientTelemetry(); + + public ComponentTelemetry General { get; private set; } + + public ComponentTelemetry Connection { get; private set; } + + public ComponentTelemetry Command { get; private set; } + + public ComponentTelemetry Transaction { get; private set; } + + private SqlClientTelemetry() + { + SqlClientMetrics clientMetrics = new(enablePlatformSpecificMetrics: true); + + General = new ComponentTelemetry(SqlClientEventSource.Log); + Connection = new ComponentTelemetry(SqlClientEventSource.Log, clientMetrics); + Command = new ComponentTelemetry(SqlClientEventSource.Log, clientMetrics); + Transaction = new ComponentTelemetry(SqlClientEventSource.Log, clientMetrics); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs new file mode 100644 index 0000000000..eba53544b1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Data.SqlClient.Telemetry +{ + // These are to be allocated at the connection pool level by default, and passed down to the connections + // which come from it. If somebody specifies additional tags then it'll need to be created on a per-connection + // basis. + // These are marked as fields to avoid the copies which come from calling property accessors. + internal sealed class ConnectionMetricTagListCollection + { + public readonly TagList GenericConnectionPoolTags; + + public readonly TagList ActiveConnectionsTags; + + public readonly TagList IdleConnectionsTags; + + public readonly TagList StasisConnectionsTags; + + public readonly TagList ReclaimedConnectionsTags; + + public readonly TagList HardConnectionsTags; + + public readonly TagList SoftConnectionsTags; + + public readonly TagList TimeoutsTags; + + public readonly TagList PendingRequestTags; + + public readonly TagList ConnectionCreationTimeTags; + + public readonly TagList ConnectionWaitTimeTags; + + public readonly TagList ConnectionUsageTimeTags; + + public readonly TagList ActiveConnectionPoolGroupsTags; + + public readonly TagList IdleConnectionPoolGroupsTags; + + public readonly TagList ActiveConnectionPoolsTags; + + public readonly TagList IdleConnectionPoolsTags; + + public readonly TagList PooledHardConnectionUsageTags; + + public readonly TagList NonPooledHardConnectionUsageTags; + + public readonly TagList HardConnectsTags; + + public readonly TagList SoftConnectsTags; + + public readonly TagList HardDisconnectsTags; + + public readonly TagList SoftDisconnectsTags; + + public ConnectionMetricTagListCollection(string poolName, params KeyValuePair[] additionalTags) + { + KeyValuePair poolNameKVP = new(MetricAttributes.PoolName, poolName); + + GenericConnectionPoolTags = new TagList() { poolNameKVP }; + + ActiveConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.ActiveState } }; + IdleConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.IdleState } }; + StasisConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.StasisState } }; + ReclaimedConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.ReclaimedState } }; + + HardConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.HardConnectionType } }; + SoftConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.SoftConnectionType } }; + + TimeoutsTags = new TagList() { poolNameKVP }; + + PendingRequestTags = new TagList() { poolNameKVP }; + + ConnectionCreationTimeTags = new TagList() { poolNameKVP }; + ConnectionWaitTimeTags = new TagList() { poolNameKVP }; + ConnectionUsageTimeTags = new TagList() { poolNameKVP }; + + ActiveConnectionPoolGroupsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.ActiveState } }; + IdleConnectionPoolGroupsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.IdleState } }; + + ActiveConnectionPoolsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.ActiveState } }; + IdleConnectionPoolsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.IdleState } }; + + PooledHardConnectionUsageTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.PooledConnectionType } }; + NonPooledHardConnectionUsageTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.NonPooledConnectionType } }; + + HardConnectsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.HardActionType } }; + SoftConnectsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.SoftActionType } }; + HardDisconnectsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.HardActionType } }; + SoftDisconnectsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.SoftActionType } }; + + foreach (KeyValuePair additionalTag in additionalTags) + { + GenericConnectionPoolTags.Add(additionalTag); + + ActiveConnectionsTags.Add(additionalTag); + IdleConnectionsTags.Add(additionalTag); + StasisConnectionsTags.Add(additionalTag); + ReclaimedConnectionsTags.Add(additionalTag); + + ActiveConnectionPoolGroupsTags.Add(additionalTag); + IdleConnectionPoolGroupsTags.Add(additionalTag); + + ActiveConnectionPoolsTags.Add(additionalTag); + IdleConnectionPoolsTags.Add(additionalTag); + + PooledHardConnectionUsageTags.Add(additionalTag); + NonPooledHardConnectionUsageTags.Add(additionalTag); + + HardConnectsTags.Add(additionalTag); + SoftConnectsTags.Add(additionalTag); + HardDisconnectsTags.Add(additionalTag); + SoftDisconnectsTags.Add(additionalTag); + } + } + } + + internal interface IMetrics + { } + + internal interface IConnectionMetrics : IMetrics + { + void HardConnectRequest(ConnectionMetricTagListCollection tagList); + + void HardDisconnectRequest(ConnectionMetricTagListCollection tagList); + + void SoftConnectRequest(ConnectionMetricTagListCollection tagList); + + void SoftDisconnectRequest(ConnectionMetricTagListCollection tagList); + + void Timeout(ConnectionMetricTagListCollection tagList); + + void ConnectionCreationTime(in TimeSpan creationTime, ConnectionMetricTagListCollection tagList); + + void ConnectionWaitTime(in TimeSpan waitTime, ConnectionMetricTagListCollection tagList); + + void ConnectionUsageTime(in TimeSpan usageTime, ConnectionMetricTagListCollection tagList); + + void EnterPendingRequest(ConnectionMetricTagListCollection tagList); + + void ExitPendingRequest(ConnectionMetricTagListCollection tagList); + + void EnterNonPooledConnection(ConnectionMetricTagListCollection tagList); + + void ExitNonPooledConnection(ConnectionMetricTagListCollection tagList); + + void EnterPooledConnection(ConnectionMetricTagListCollection tagList); + + void ExitPooledConnection(ConnectionMetricTagListCollection tagList); + + void EnterActiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList); + + void ExitActiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList); + + void EnterInactiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList); + + void ExitInactiveConnectionPoolGroup(ConnectionMetricTagListCollection tagList); + + void EnterActiveConnectionPool(ConnectionMetricTagListCollection tagList); + + void ExitActiveConnectionPool(ConnectionMetricTagListCollection tagList); + + void EnterInactiveConnectionPool(ConnectionMetricTagListCollection tagList); + + void ExitInactiveConnectionPool(ConnectionMetricTagListCollection tagList); + + void EnterActiveConnection(ConnectionMetricTagListCollection tagList); + + void ExitActiveConnection(ConnectionMetricTagListCollection tagList); + + void EnterFreeConnection(ConnectionMetricTagListCollection tagList); + + void ExitFreeConnection(ConnectionMetricTagListCollection tagList); + + void EnterStasisConnection(ConnectionMetricTagListCollection tagList); + + void ExitStasisConnection(ConnectionMetricTagListCollection tagList); + + void ReclaimedConnectionRequest(ConnectionMetricTagListCollection tagList); + } + + internal interface ITransactionMetrics : IMetrics + { + // + } + + internal interface ICommandMetrics : IMetrics + { + // + } +} From 840a2347af1a4ba11fbce3931195d42f97f5371a Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:53:14 +0000 Subject: [PATCH 2/6] Renamed MetricAttributes and MetricAttributeValues to MetricTagNames and MetricTagValues --- .../Telemetry/SqlClientMetrics.NetCoreApp.cs | 40 +++++++++---------- .../SqlClient/Telemetry/MetricConstants.cs | 4 +- .../SqlClient/Telemetry/SqlClientMetrics.cs | 4 +- .../Telemetry/TelemetryInterfaces.cs | 34 ++++++++-------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs index d90487dc27..21d9e4545f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.NetCoreApp.cs @@ -214,81 +214,81 @@ private ref long GetPlatformSpecificMetric(string metricName, in TagList tagList switch (metricName) { case MetricNames.Connections.Usage: - associatedTag = GetTagByName(MetricAttributes.State, 1, in tagList); + associatedTag = GetTagByName(MetricTagNames.State, 1, in tagList); - if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + if (string.Equals((string)associatedTag.Value, MetricTagValues.ActiveState, StringComparison.OrdinalIgnoreCase)) { return ref _activeConnectionsCounter; } - else if(string.Equals((string)associatedTag.Value, MetricAttributeValues.IdleState, StringComparison.OrdinalIgnoreCase)) + else if(string.Equals((string)associatedTag.Value, MetricTagValues.IdleState, StringComparison.OrdinalIgnoreCase)) { return ref _freeConnectionsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.StasisState, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.StasisState, StringComparison.OrdinalIgnoreCase)) { return ref _stasisConnectionsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ReclaimedState, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.ReclaimedState, StringComparison.OrdinalIgnoreCase)) { return ref _reclaimedConnectionsCounter; } break; case MetricNames.ConnectionPoolGroups.Usage: - associatedTag = GetTagByName(MetricAttributes.State, 1, in tagList); + associatedTag = GetTagByName(MetricTagNames.State, 1, in tagList); - if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + if (string.Equals((string)associatedTag.Value, MetricTagValues.ActiveState, StringComparison.OrdinalIgnoreCase)) { return ref _activeConnectionPoolGroupsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.IdleState, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.IdleState, StringComparison.OrdinalIgnoreCase)) { return ref _inactiveConnectionPoolGroupsCounter; } break; case MetricNames.ConnectionPools.Usage: - associatedTag = GetTagByName(MetricAttributes.State, 1, in tagList); + associatedTag = GetTagByName(MetricTagNames.State, 1, in tagList); - if (string.Equals((string)associatedTag.Value, MetricAttributeValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + if (string.Equals((string)associatedTag.Value, MetricTagValues.ActiveState, StringComparison.OrdinalIgnoreCase)) { return ref _activeConnectionPoolsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.IdleState, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.IdleState, StringComparison.OrdinalIgnoreCase)) { return ref _inactiveConnectionPoolsCounter; } break; case MetricNames.Connections.HardUsage: - associatedTag = GetTagByName(MetricAttributes.Type, 1, in tagList); + associatedTag = GetTagByName(MetricTagNames.Type, 1, in tagList); - if (string.Equals((string)associatedTag.Value, MetricAttributeValues.PooledConnectionType, StringComparison.OrdinalIgnoreCase)) + if (string.Equals((string)associatedTag.Value, MetricTagValues.PooledConnectionType, StringComparison.OrdinalIgnoreCase)) { return ref _pooledConnectionsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.NonPooledConnectionType, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.NonPooledConnectionType, StringComparison.OrdinalIgnoreCase)) { return ref _nonPooledConnectionsCounter; } break; case MetricNames.Connections.Connects: - associatedTag = GetTagByName(MetricAttributes.Type, 1, in tagList); + associatedTag = GetTagByName(MetricTagNames.Type, 1, in tagList); - if (string.Equals((string)associatedTag.Value, MetricAttributeValues.HardActionType, StringComparison.OrdinalIgnoreCase)) + if (string.Equals((string)associatedTag.Value, MetricTagValues.HardActionType, StringComparison.OrdinalIgnoreCase)) { return ref _hardConnectsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.SoftActionType, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.SoftActionType, StringComparison.OrdinalIgnoreCase)) { return ref _softConnectsCounter; } break; case MetricNames.Connections.Disconnects: - associatedTag = GetTagByName(MetricAttributes.Type, 1, in tagList); + associatedTag = GetTagByName(MetricTagNames.Type, 1, in tagList); - if (string.Equals((string)associatedTag.Value, MetricAttributeValues.HardActionType, StringComparison.OrdinalIgnoreCase)) + if (string.Equals((string)associatedTag.Value, MetricTagValues.HardActionType, StringComparison.OrdinalIgnoreCase)) { return ref _hardDisconnectsCounter; } - else if (string.Equals((string)associatedTag.Value, MetricAttributeValues.SoftActionType, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals((string)associatedTag.Value, MetricTagValues.SoftActionType, StringComparison.OrdinalIgnoreCase)) { return ref _softDisconnectsCounter; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs index 6ed648d141..a5d0ed7aba 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/MetricConstants.cs @@ -100,7 +100,7 @@ public static class Transactions } } - internal static class MetricAttributes + internal static class MetricTagNames { public const string PoolName = "pool.name"; @@ -111,7 +111,7 @@ internal static class MetricAttributes public const string Type = "type"; } - internal static class MetricAttributeValues + internal static class MetricTagValues { public const string IdleState = "idle"; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs index 1ea7d0ff07..164b84fd53 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs @@ -293,7 +293,7 @@ private IEnumerable> GetMinConnectionPoolSizes() foreach (KeyValuePair poolMetric in connectionPoolMetrics) yield return new Measurement(poolMetric.Value.TotalMinPoolSize, - new KeyValuePair(MetricAttributes.PoolName, poolMetric.Key)); + new KeyValuePair(MetricTagNames.PoolName, poolMetric.Key)); } private IEnumerable> GetMaxConnectionPoolSizes() @@ -302,7 +302,7 @@ private IEnumerable> GetMaxConnectionPoolSizes() foreach (KeyValuePair poolMetric in connectionPoolMetrics) yield return new Measurement(poolMetric.Value.TotalMaxPoolSize, - new KeyValuePair(MetricAttributes.PoolName, poolMetric.Key)); + new KeyValuePair(MetricTagNames.PoolName, poolMetric.Key)); } #endregion } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs index eba53544b1..6be453a448 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/TelemetryInterfaces.cs @@ -60,17 +60,17 @@ internal sealed class ConnectionMetricTagListCollection public ConnectionMetricTagListCollection(string poolName, params KeyValuePair[] additionalTags) { - KeyValuePair poolNameKVP = new(MetricAttributes.PoolName, poolName); + KeyValuePair poolNameKVP = new(MetricTagNames.PoolName, poolName); GenericConnectionPoolTags = new TagList() { poolNameKVP }; - ActiveConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.ActiveState } }; - IdleConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.IdleState } }; - StasisConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.StasisState } }; - ReclaimedConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.State, MetricAttributeValues.ReclaimedState } }; + ActiveConnectionsTags = new TagList() { poolNameKVP, { MetricTagNames.State, MetricTagValues.ActiveState } }; + IdleConnectionsTags = new TagList() { poolNameKVP, { MetricTagNames.State, MetricTagValues.IdleState } }; + StasisConnectionsTags = new TagList() { poolNameKVP, { MetricTagNames.State, MetricTagValues.StasisState } }; + ReclaimedConnectionsTags = new TagList() { poolNameKVP, { MetricTagNames.State, MetricTagValues.ReclaimedState } }; - HardConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.HardConnectionType } }; - SoftConnectionsTags = new TagList() { poolNameKVP, { MetricAttributes.Type, MetricAttributeValues.SoftConnectionType } }; + HardConnectionsTags = new TagList() { poolNameKVP, { MetricTagNames.Type, MetricTagValues.HardConnectionType } }; + SoftConnectionsTags = new TagList() { poolNameKVP, { MetricTagNames.Type, MetricTagValues.SoftConnectionType } }; TimeoutsTags = new TagList() { poolNameKVP }; @@ -80,19 +80,19 @@ public ConnectionMetricTagListCollection(string poolName, params KeyValuePair additionalTag in additionalTags) { From 84ec5fbb912b24fe69280180309147f21fd6a5e1 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:40:17 +0100 Subject: [PATCH 3/6] Forcing the usage of System.Diagnostics.DiagnosticSource 8.0.0 on .NET 6.0. This allows the use of UpDownCounter for usage counters. --- .../src/Microsoft.Data.SqlClient.csproj | 4 ++- .../SqlClient/Telemetry/SqlClientMetrics.cs | 29 ++++++++++++------- tools/props/Versions.props | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index aa87d9836e..ac9dd64771 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -1010,7 +1010,6 @@ - @@ -1020,6 +1019,9 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs index 164b84fd53..6cf0f2bffb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.cs @@ -26,7 +26,7 @@ internal sealed partial class SqlClientMetrics : IDisposable, IConnectionMetrics /// Extended: db.client.connections.usage{pool.name="", state="stasis"} /// Extended: db.client.connections.usage{pool.name="", type="hard|soft"} /// - private readonly Counter _connectionUsageCounter; + private readonly UpDownCounter _connectionUsageCounter; /// /// Standard: db.client.connections.idle.max{pool.name=""} /// @@ -42,7 +42,7 @@ internal sealed partial class SqlClientMetrics : IDisposable, IConnectionMetrics /// /// Standard: db.client.connections.pending_requests{pool.name=""} /// - private readonly Counter _connectionPendingRequestsCounter; + private readonly UpDownCounter _connectionPendingRequestsCounter; /// /// Standard: db.client.connections.timeouts{pool.name=""} /// @@ -62,15 +62,15 @@ internal sealed partial class SqlClientMetrics : IDisposable, IConnectionMetrics /// /// Extended: sqlclient.db.client.connection_pools.usage{pool.name="", state="active|idle"} /// - private readonly Counter _connectionPoolUsageCounter; + private readonly UpDownCounter _connectionPoolUsageCounter; /// /// Extended: sqlclient.db.client.connection_pool_groups.usage{pool.name="", state="active|idle"} /// - private readonly Counter _connectionPoolGroupUsageCounter; + private readonly UpDownCounter _connectionPoolGroupUsageCounter; /// /// Extended: sqlclient.db.client.connections.hard.usage{pool.name="", type="pooled|unpooled"} /// - private readonly Counter _connectionHardUsageCounter; + private readonly UpDownCounter _connectionHardUsageCounter; /// /// Extended: sqlclient.db.client.connections.connects{pool.name="", type="hard|soft"} /// @@ -85,7 +85,7 @@ public SqlClientMetrics(bool enablePlatformSpecificMetrics) _enablePlatformSpecificMetrics = enablePlatformSpecificMetrics; _meter = new Meter("Microsoft.Data.SqlClient", MeteringVersion); - _connectionUsageCounter = _meter.CreateCounter(MetricNames.Connections.Usage, MetricUnits.Connection); + _connectionUsageCounter = _meter.CreateUpDownCounter(MetricNames.Connections.Usage, MetricUnits.Connection); _connectionMaxIdleCounter = _meter.CreateObservableCounter(MetricNames.Connections.MaxIdle, GetMaxConnectionPoolSizes, MetricUnits.Connection); _connectionMinIdleCounter = _meter.CreateObservableCounter(MetricNames.Connections.MinIdle, GetMinConnectionPoolSizes, MetricUnits.Connection); _connectionMaxCounter = _meter.CreateObservableCounter(MetricNames.Connections.Max, GetMaxConnectionPoolSizes, MetricUnits.Connection); @@ -99,7 +99,7 @@ public SqlClientMetrics(bool enablePlatformSpecificMetrics) // 3. It times out // 4. Its underlying connection times out/is broken // It does not leave and re-enter the pending state when it's being retried. - _connectionPendingRequestsCounter = _meter.CreateCounter(MetricNames.Connections.PendingRequests, MetricUnits.Request); + _connectionPendingRequestsCounter = _meter.CreateUpDownCounter(MetricNames.Connections.PendingRequests, MetricUnits.Request); // This is to be reported from TdsParserStateObject.OnTimeoutCore for both .NET Core and Framework. The connection string will come from // _parser.Connection.ConnectionOptions.UsersConnectionStringForTrace. @@ -125,9 +125,9 @@ public SqlClientMetrics(bool enablePlatformSpecificMetrics) // This is the time difference between the connection's open and close timestamps. It is not affected by disabling and enabling SqlStatistics. _connectionUsageTimeHistogram = _meter.CreateHistogram(MetricNames.Connections.UsageTime, MetricUnits.Millisecond); - _connectionPoolUsageCounter = _meter.CreateCounter(MetricNames.ConnectionPools.Usage, MetricUnits.ConnectionPool); - _connectionPoolGroupUsageCounter = _meter.CreateCounter(MetricNames.ConnectionPoolGroups.Usage, MetricUnits.ConnectionPoolGroup); - _connectionHardUsageCounter = _meter.CreateCounter(MetricNames.Connections.HardUsage, MetricUnits.Connection); + _connectionPoolUsageCounter = _meter.CreateUpDownCounter(MetricNames.ConnectionPools.Usage, MetricUnits.ConnectionPool); + _connectionPoolGroupUsageCounter = _meter.CreateUpDownCounter(MetricNames.ConnectionPoolGroups.Usage, MetricUnits.ConnectionPoolGroup); + _connectionHardUsageCounter = _meter.CreateUpDownCounter(MetricNames.Connections.HardUsage, MetricUnits.Connection); _connectionConnectCounter = _meter.CreateCounter(MetricNames.Connections.Connects, MetricUnits.Connect); _connectionDisconnectCounter = _meter.CreateCounter(MetricNames.Connections.Disconnects, MetricUnits.Disconnect); @@ -165,6 +165,15 @@ public void Dispose() // This method is designed to handle backwards compatibility with the older performance counters (.NET Framework) and event counters. // Since they're all counters, we only need to implement the compatibility layer here. private void WriteInstrumentValue(Counter counter, long value, in TagList tagList) + { + counter.Add(value, in tagList); + if (_enablePlatformSpecificMetrics && value > 0) + { + IncrementPlatformSpecificMetric(counter.Name, in tagList); + } + } + + private void WriteInstrumentValue(UpDownCounter counter, long value, in TagList tagList) { counter.Add(value, in tagList); if (_enablePlatformSpecificMetrics) diff --git a/tools/props/Versions.props b/tools/props/Versions.props index c16174881d..81bf0788ed 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -51,7 +51,7 @@ 4.3.0 - 6.0.1 + 8.0.0 From 3e73d3f5aa2062da7cab14e19769b9f7f9bcb7dd Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:35:44 +0100 Subject: [PATCH 4/6] Enabled .NET Framework project to build --- .../netfx/src/Microsoft.Data.SqlClient.csproj | 22 +++++++++++++++ .../Data/ProviderBase/DbConnectionFactory.cs | 27 +++++++++++++++++++ .../Telemetry/SqlClientMetrics.netfx.cs | 19 +++++++++++++ tools/props/Versions.props | 1 + 4 files changed, 69 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 0a0757731b..6213fb9169 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -346,6 +346,24 @@ Microsoft\Data\SqlClient\Server\SqlSer.cs + + Microsoft\Data\SqlClient\Telemetry\ComponentTelemetry.cs + + + Microsoft\Data\SqlClient\Telemetry\MetricConstants.cs + + + Microsoft\Data\SqlClient\Telemetry\SqlClientMetrics.cs + + + Microsoft\Data\SqlClient\Telemetry\SqlClientTelemetry.cs + + + Microsoft\Data\SqlClient\Telemetry\DbConnectionFactoryTelemetry.cs + + + Microsoft\Data\SqlClient\Telemetry\TelemetryInterfaces.cs + Microsoft\Data\SqlClient\SignatureVerificationCache.cs @@ -648,6 +666,7 @@ + @@ -745,6 +764,9 @@ $(SystemBuffersVersion) + + $(SystemDiagnosticsDiagnosticSourceVersion) + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs index 701719796a..e0bd56eb82 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs @@ -13,6 +13,7 @@ namespace Microsoft.Data.ProviderBase using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.SqlClient; + using Microsoft.Data.SqlClient.Telemetry; internal abstract class DbConnectionFactory { @@ -93,6 +94,32 @@ public void ClearPool(DbConnection connection) } } + public Dictionary GetFactoryMetrics() + { + Dictionary connectionPoolGroups = _connectionPoolGroups; + Dictionary telemetry = new(connectionPoolGroups.Count); + + foreach (DbConnectionPoolGroup poolGroup in connectionPoolGroups.Values) + { + // This will ensure that the connection string has does not reveal its credentials + string connectionString = poolGroup.ConnectionOptions.UsersConnectionStringForTrace(); + + // This will return true if the connection string uses integrated authentication and a second connection has + // been opened in an impersonated context. The specification doesn't handle this well, so the only thing to + // do is to roll all of the contexts up into a single connection string. + if (telemetry.ContainsKey(connectionString)) + { + telemetry[connectionString].AddConnectionPool(poolGroup.Telemetry); + } + else + { + telemetry.Add(connectionString, new DbConnectionFactoryTelemetry(poolGroup.Telemetry)); + } + } + + return telemetry; + } + public void ClearPool(DbConnectionPoolKey key) { Debug.Assert(key != null, "key cannot be null"); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs new file mode 100644 index 0000000000..1ead87c55f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace Microsoft.Data.SqlClient.Telemetry +{ + internal sealed partial class SqlClientMetrics + { + private void InitializePlatformSpecificMetrics() { } + + private void IncrementPlatformSpecificMetric(string metricName, in TagList tagList) { } + + private void DecrementPlatformSpecificMetric(string metricName, in TagList tagList) { } + + private void DisposePlatformSpecificMetrics() { } + } +} diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 81bf0788ed..c516ecb61b 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -24,6 +24,7 @@ 5.2.0 + 8.0.0 From 157c09a164af21b615460a32013149e77a85f791 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:00:13 +0100 Subject: [PATCH 5/6] Added .NET Framework performance counters to SqlClientMetrics --- .../Telemetry/SqlClientMetrics.netfx.cs | 174 +++++++++++++++++- 1 file changed, 172 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs index 1ead87c55f..41ab3589f9 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs @@ -2,18 +2,188 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Diagnostics; +using System.Reflection; +using System.Runtime.ConstrainedExecution; +using System.Runtime.Versioning; +using System.Security.Permissions; +using Microsoft.Data.Common; namespace Microsoft.Data.SqlClient.Telemetry { + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] internal sealed partial class SqlClientMetrics { - private void InitializePlatformSpecificMetrics() { } + private const string PerformanceCounterCategoryName = ".NET Data Provider for SqlServer"; + private const string PerformanceCounterCategoryHelp = "Counters for Microsoft.Data.SqlClient"; + + private const int PerformanceCounterInstanceNameMaxLength = 127; + + private static string s_assemblyName = GetAssemblyName(); + private static string s_instanceName = GetInstanceName(); + + private PerformanceCounter _hardConnectsPerSecond; + private PerformanceCounter _hardDisconnectsPerSecond; + + private PerformanceCounter _softConnectsPerSecond; + private PerformanceCounter _softDisconnectsPerSecond; + + private PerformanceCounter _numberOfNonPooledConnections; + private PerformanceCounter _numberOfPooledConnections; + + private PerformanceCounter _numberOfActiveConnectionPoolGroups; + private PerformanceCounter _numberOfInactiveConnectionPoolGroups; + + private PerformanceCounter _numberOfActiveConnectionPools; + private PerformanceCounter _numberOfInactiveConnectionPools; + + private PerformanceCounter _numberOfActiveConnections; + private PerformanceCounter _numberOfFreeConnections; + + private PerformanceCounter _numberOfStasisConnections; + private PerformanceCounter _numberOfReclaimedConnections; + + private void InitializePlatformSpecificMetrics() + { + AppDomain.CurrentDomain.DomainUnload += UnloadEventHandler; + AppDomain.CurrentDomain.ProcessExit += ExitEventHandler; + AppDomain.CurrentDomain.UnhandledException += ExceptionEventHandler; + + _hardConnectsPerSecond = CreatePerformanceCounter("HardConnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); + _hardDisconnectsPerSecond = CreatePerformanceCounter("HardDisconnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); + + _softConnectsPerSecond = CreatePerformanceCounter("SoftConnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); + _softDisconnectsPerSecond = CreatePerformanceCounter("SoftDisconnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); + + _numberOfNonPooledConnections = CreatePerformanceCounter("NumberOfNonPooledConnections", PerformanceCounterType.NumberOfItems64); + _numberOfPooledConnections = CreatePerformanceCounter("NumberOfPooledConnections", PerformanceCounterType.NumberOfItems64); + + _numberOfActiveConnectionPoolGroups = CreatePerformanceCounter("NumberOfActiveConnectionPoolGroups", PerformanceCounterType.NumberOfItems64); + _numberOfInactiveConnectionPoolGroups = CreatePerformanceCounter("NumberOfInactiveConnectionPoolGroups", PerformanceCounterType.NumberOfItems64); + + _numberOfActiveConnectionPools = CreatePerformanceCounter("NumberOfActiveConnectionPools", PerformanceCounterType.NumberOfItems64); + _numberOfInactiveConnectionPools = CreatePerformanceCounter("NumberOfInactiveConnectionPools", PerformanceCounterType.NumberOfItems64); + + _numberOfActiveConnections = CreatePerformanceCounter("NumberOfActiveConnections", PerformanceCounterType.NumberOfItems64); + _numberOfFreeConnections = CreatePerformanceCounter("NumberOfFreeConnections", PerformanceCounterType.NumberOfItems64); + + _numberOfStasisConnections = CreatePerformanceCounter("NumberOfStasisConnections", PerformanceCounterType.NumberOfItems64); + _numberOfReclaimedConnections = CreatePerformanceCounter("NumberOfReclaimedConnections", PerformanceCounterType.NumberOfItems64); + } private void IncrementPlatformSpecificMetric(string metricName, in TagList tagList) { } private void DecrementPlatformSpecificMetric(string metricName, in TagList tagList) { } - private void DisposePlatformSpecificMetrics() { } + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + private void DisposePlatformSpecificMetrics() + { + _hardConnectsPerSecond?.Dispose(); + _hardDisconnectsPerSecond?.Dispose(); + + _softConnectsPerSecond?.Dispose(); + _softDisconnectsPerSecond?.Dispose(); + + _numberOfNonPooledConnections?.Dispose(); + _numberOfPooledConnections?.Dispose(); + + _numberOfActiveConnectionPoolGroups?.Dispose(); + _numberOfInactiveConnectionPoolGroups?.Dispose(); + + _numberOfActiveConnectionPools?.Dispose(); + _numberOfInactiveConnectionPools?.Dispose(); + + _numberOfActiveConnections?.Dispose(); + _numberOfFreeConnections?.Dispose(); + + _numberOfStasisConnections?.Dispose(); + _numberOfReclaimedConnections?.Dispose(); + } + + [FileIOPermission(SecurityAction.Assert, Unrestricted = true)] + private static string GetAssemblyName() + => Assembly.GetExecutingAssembly()?.GetName()?.Name + ?? AppDomain.CurrentDomain?.FriendlyName; + + // SxS: this method uses GetCurrentProcessId to construct the instance name. + // TODO: VSDD 534795 - remove the Resource* attributes if you do not use GetCurrentProcessId after the fix + [ResourceExposure(ResourceScope.None)] + [ResourceConsumption(ResourceScope.Process, ResourceScope.Process)] + private static string GetInstanceName() + { + // TODO: If you do not use GetCurrentProcessId after fixing VSDD 534795, please remove Resource* attributes from this method + int pid = Microsoft.Data.Common.SafeNativeMethods.GetCurrentProcessId(); + + // SQLBUDT #366157 -there are several characters which have special meaning + // to PERFMON. They recommend that we translate them as shown below, to + // prevent problems. + + string result = string.Format(null, "{0}[{1}]", s_assemblyName, pid); + result = result.Replace('(', '[').Replace(')', ']').Replace('#', '_').Replace('/', '_').Replace('\\', '_'); + + // SQLBUVSTS #94625 - counter instance name cannot be greater than 127 + if (result.Length > PerformanceCounterInstanceNameMaxLength) + { + // Replacing the middle part with "[...]" + // For example: if path is c:\long_path\very_(Ax200)_long__path\perftest.exe and process ID is 1234 than the resulted instance name will be: + // c:\long_path\very_(AxM)[...](AxN)_long__path\perftest.exe[1234] + // while M and N are adjusted to make each part before and after the [...] = 61 (making the total = 61 + 5 + 61 = 127) + const string insertString = "[...]"; + int firstPartLength = (PerformanceCounterInstanceNameMaxLength - insertString.Length) / 2; + int lastPartLength = PerformanceCounterInstanceNameMaxLength - firstPartLength - insertString.Length; + result = string.Format(null, "{0}{1}{2}", + result.Substring(0, firstPartLength), + insertString, + result.Substring(result.Length - lastPartLength, lastPartLength)); + + Debug.Assert(result.Length == PerformanceCounterInstanceNameMaxLength, + string.Format(null, "wrong calculation of the instance name: expected {0}, actual: {1}", PerformanceCounterInstanceNameMaxLength, result.Length)); + } + + return result; + } + + private PerformanceCounter CreatePerformanceCounter(string counterName, PerformanceCounterType counterType) + { + try + { + return new PerformanceCounter() + { + CategoryName = PerformanceCounterCategoryName, + CounterName = counterName, + InstanceName = s_instanceName, + InstanceLifetime = PerformanceCounterInstanceLifetime.Process, + ReadOnly = false, + RawValue = 0 + }; + } + catch(InvalidOperationException e) + { + ADP.TraceExceptionWithoutRethrow(e); + return null; + } + } + + [PrePrepareMethod] + void ExceptionEventHandler(object sender, UnhandledExceptionEventArgs e) + { + if ((null != e) && e.IsTerminating) + { + Dispose(); + } + } + + [PrePrepareMethod] + void ExitEventHandler(object sender, EventArgs e) + { + Dispose(); + } + + [PrePrepareMethod] + void UnloadEventHandler(object sender, EventArgs e) + { + Dispose(); + } } } From 4d437278b40c4d4b880254c1899881a27f6ad2dd Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:46:33 +0100 Subject: [PATCH 6/6] Added performance counter adjustment code. This hasn't been connected to anything unique to .NET Framework yet, and hasn't been tested. --- .../Telemetry/SqlClientMetrics.netfx.cs | 163 +++++++++++++++++- 1 file changed, 154 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs index 41ab3589f9..c39a9eccec 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/Telemetry/SqlClientMetrics.netfx.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.ConstrainedExecution; @@ -16,7 +17,6 @@ namespace Microsoft.Data.SqlClient.Telemetry internal sealed partial class SqlClientMetrics { private const string PerformanceCounterCategoryName = ".NET Data Provider for SqlServer"; - private const string PerformanceCounterCategoryHelp = "Counters for Microsoft.Data.SqlClient"; private const int PerformanceCounterInstanceNameMaxLength = 127; @@ -46,6 +46,8 @@ internal sealed partial class SqlClientMetrics private void InitializePlatformSpecificMetrics() { + TraceSwitch perfCtrSwitch = new TraceSwitch("ConnectionPoolPerformanceCounterDetail", "level of detail to track with connection pool performance counters"); + AppDomain.CurrentDomain.DomainUnload += UnloadEventHandler; AppDomain.CurrentDomain.ProcessExit += ExitEventHandler; AppDomain.CurrentDomain.UnhandledException += ExceptionEventHandler; @@ -53,9 +55,6 @@ private void InitializePlatformSpecificMetrics() _hardConnectsPerSecond = CreatePerformanceCounter("HardConnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); _hardDisconnectsPerSecond = CreatePerformanceCounter("HardDisconnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); - _softConnectsPerSecond = CreatePerformanceCounter("SoftConnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); - _softDisconnectsPerSecond = CreatePerformanceCounter("SoftDisconnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); - _numberOfNonPooledConnections = CreatePerformanceCounter("NumberOfNonPooledConnections", PerformanceCounterType.NumberOfItems64); _numberOfPooledConnections = CreatePerformanceCounter("NumberOfPooledConnections", PerformanceCounterType.NumberOfItems64); @@ -65,16 +64,38 @@ private void InitializePlatformSpecificMetrics() _numberOfActiveConnectionPools = CreatePerformanceCounter("NumberOfActiveConnectionPools", PerformanceCounterType.NumberOfItems64); _numberOfInactiveConnectionPools = CreatePerformanceCounter("NumberOfInactiveConnectionPools", PerformanceCounterType.NumberOfItems64); - _numberOfActiveConnections = CreatePerformanceCounter("NumberOfActiveConnections", PerformanceCounterType.NumberOfItems64); - _numberOfFreeConnections = CreatePerformanceCounter("NumberOfFreeConnections", PerformanceCounterType.NumberOfItems64); - _numberOfStasisConnections = CreatePerformanceCounter("NumberOfStasisConnections", PerformanceCounterType.NumberOfItems64); _numberOfReclaimedConnections = CreatePerformanceCounter("NumberOfReclaimedConnections", PerformanceCounterType.NumberOfItems64); + + if (perfCtrSwitch.TraceVerbose) + { + _softConnectsPerSecond = CreatePerformanceCounter("SoftConnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); + _softDisconnectsPerSecond = CreatePerformanceCounter("SoftDisconnectsPerSecond", PerformanceCounterType.RateOfCountsPerSecond64); + + _numberOfActiveConnections = CreatePerformanceCounter("NumberOfActiveConnections", PerformanceCounterType.NumberOfItems64); + _numberOfFreeConnections = CreatePerformanceCounter("NumberOfFreeConnections", PerformanceCounterType.NumberOfItems64); + } + } + + private void IncrementPlatformSpecificMetric(string metricName, in TagList tagList) + { + PerformanceCounter counter = GetPlatformSpecificMetric(metricName, in tagList, out bool successful); + + if (successful) + { + counter.Increment(); + } } - private void IncrementPlatformSpecificMetric(string metricName, in TagList tagList) { } + private void DecrementPlatformSpecificMetric(string metricName, in TagList tagList) + { + PerformanceCounter counter = GetPlatformSpecificMetric(metricName, in tagList, out bool successful); - private void DecrementPlatformSpecificMetric(string metricName, in TagList tagList) { } + if (successful) + { + counter.Decrement(); + } + } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] private void DisposePlatformSpecificMetrics() @@ -185,5 +206,129 @@ void UnloadEventHandler(object sender, EventArgs e) { Dispose(); } + + private KeyValuePair GetTagByName(string tagName, int likelyIndex, in TagList tagList) + { + KeyValuePair tagValue; + + // We have control over the initial tag list, so in almost every circumstance this shortcut will be used. + // It spares us from a loop, however small. + // In most cases, index 0 is the connection pool name. + if (likelyIndex > 0 && likelyIndex < tagList.Count) + { + tagValue = tagList[likelyIndex]; + + if (string.Equals(tagValue.Key, tagName, StringComparison.OrdinalIgnoreCase)) + { + return tagValue; + } + } + + for (int i = 0; i < tagList.Count; i++) + { + tagValue = tagList[i]; + + if (string.Equals(tagValue.Key, tagName, StringComparison.OrdinalIgnoreCase)) + { + return tagValue; + } + } + + throw ADP.CollectionIndexString(typeof(KeyValuePair), nameof(KeyValuePair.Key), tagName, typeof(TagList)); + } + + private PerformanceCounter GetPlatformSpecificMetric(string metricName, in TagList tagList, out bool successful) + { + KeyValuePair associatedTag; + + successful = true; + + switch (metricName) + { + case MetricNames.Connections.Usage: + associatedTag = GetTagByName(MetricTagNames.State, 1, in tagList); + if (string.Equals((string)associatedTag.Value, MetricTagValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + { + successful = _numberOfActiveConnections != null; + return _numberOfActiveConnections; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.IdleState, StringComparison.OrdinalIgnoreCase)) + { + successful = _numberOfFreeConnections != null; + return _numberOfFreeConnections; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.StasisState, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfStasisConnections; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.ReclaimedState, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfReclaimedConnections; + } + break; + case MetricNames.ConnectionPoolGroups.Usage: + associatedTag = GetTagByName(MetricTagNames.State, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricTagValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfActiveConnectionPoolGroups; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.IdleState, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfInactiveConnectionPoolGroups; + } + break; + case MetricNames.ConnectionPools.Usage: + associatedTag = GetTagByName(MetricTagNames.State, 1, in tagList); + + if (string.Equals((string)associatedTag.Value, MetricTagValues.ActiveState, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfActiveConnectionPools; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.IdleState, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfInactiveConnectionPools; + } + break; + case MetricNames.Connections.HardUsage: + associatedTag = GetTagByName(MetricTagNames.Type, 1, in tagList); + if (string.Equals((string)associatedTag.Value, MetricTagValues.PooledConnectionType, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfPooledConnections; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.NonPooledConnectionType, StringComparison.OrdinalIgnoreCase)) + { + return _numberOfNonPooledConnections; + } + break; + case MetricNames.Connections.Connects: + associatedTag = GetTagByName(MetricTagNames.Type, 1, in tagList); + if (string.Equals((string)associatedTag.Value, MetricTagValues.HardActionType, StringComparison.OrdinalIgnoreCase)) + { + return _hardConnectsPerSecond; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.SoftConnectionType, StringComparison.OrdinalIgnoreCase)) + { + successful = _softConnectsPerSecond != null; + return _softConnectsPerSecond; + } + break; + case MetricNames.Connections.Disconnects: + associatedTag = GetTagByName(MetricTagNames.Type, 1, in tagList); + if (string.Equals((string)associatedTag.Value, MetricTagValues.HardActionType, StringComparison.OrdinalIgnoreCase)) + { + return _hardDisconnectsPerSecond; + } + else if (string.Equals((string)associatedTag.Value, MetricTagValues.SoftConnectionType, StringComparison.OrdinalIgnoreCase)) + { + successful = _softDisconnectsPerSecond != null; + return _softDisconnectsPerSecond; + } + break; + } + + successful = false; + return null; + } } }