Cache tags from DbCommand (#774)

* Cache the tags extracted from connection strings
* Disable the cache if too many different connection strings are used
This commit is contained in:
Kevin Gosse 2020-07-02 10:52:48 +02:00 committed by GitHub
parent 6a1abe0a45
commit 382ea15cb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 207 additions and 24 deletions

View File

@ -1,6 +1,7 @@
using System;
using System.Data;
using System.Data.Common;
using Datadog.Trace.Util;
namespace Datadog.Trace.ExtensionMethods
{
@ -34,17 +35,12 @@ namespace Datadog.Trace.ExtensionMethods
span.ResourceName = command.CommandText;
span.Type = SpanTypes.Sql;
// parse the connection string
var builder = new DbConnectionStringBuilder { ConnectionString = command.Connection.ConnectionString };
var tags = DbCommandCache.GetTagsFromDbCommand(command);
string database = GetConnectionStringValue(builder, "Database", "Initial Catalog", "InitialCatalog");
span.SetTag(Tags.DbName, database);
string user = GetConnectionStringValue(builder, "User ID", "UserID");
span.SetTag(Tags.DbUser, user);
string server = GetConnectionStringValue(builder, "Server", "Data Source", "DataSource", "Network Address", "NetworkAddress", "Address", "Addr", "Host");
span.SetTag(Tags.OutHost, server);
foreach (var pair in tags)
{
span.SetTag(pair.Key, pair.Value);
}
}
internal static void DecorateWebServerSpan(
@ -62,19 +58,5 @@ namespace Datadog.Trace.ExtensionMethods
span.SetTag(Tags.HttpUrl, httpUrl);
span.SetTag(Tags.Language, TracerConstants.Language);
}
private static string GetConnectionStringValue(DbConnectionStringBuilder builder, params string[] names)
{
foreach (string name in names)
{
if (builder.TryGetValue(name, out object valueObj) &&
valueObj is string value)
{
return value;
}
}
return null;
}
}
}

View File

@ -0,0 +1,104 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Threading;
using Datadog.Trace.Logging;
using Datadog.Trace.Vendors.Serilog;
namespace Datadog.Trace.Util
{
internal static class DbCommandCache
{
internal const int MaxConnectionStrings = 100;
private static readonly ILogger Log = DatadogLogging.GetLogger(typeof(DbCommandCache));
private static ConcurrentDictionary<string, KeyValuePair<string, string>[]> _cache
= new ConcurrentDictionary<string, KeyValuePair<string, string>[]>();
/// <summary>
/// Gets or sets the underlying cache, to be used for unit tests
/// </summary>
internal static ConcurrentDictionary<string, KeyValuePair<string, string>[]> Cache
{
get
{
return _cache;
}
set
{
_cache = value;
}
}
public static KeyValuePair<string, string>[] GetTagsFromDbCommand(IDbCommand command)
{
var connectionString = command.Connection.ConnectionString;
var cache = _cache;
if (cache != null)
{
if (cache.TryGetValue(connectionString, out var tags))
{
// Fast path: it's expected that most calls will end up in this branch
return tags;
}
if (cache.Count <= MaxConnectionStrings)
{
// Populating the cache. This path should be hit only during application warmup
// ReSharper disable once ConvertClosureToMethodGroup -- Lambdas are cached by the compiler
return cache.GetOrAdd(connectionString, cs => ExtractTagsFromConnectionString(cs));
}
// The assumption "connection strings are a finite set" was wrong, disabling the cache
// Use atomic operation to log only once
if (Interlocked.Exchange(ref _cache, null) != null)
{
Log.Information($"More than {MaxConnectionStrings} different connection strings were used, disabling cache");
}
}
// Fallback: too many different connection string, there might be a random part in them
// Stop using the cache to prevent memory leaks
return ExtractTagsFromConnectionString(connectionString);
}
private static KeyValuePair<string, string>[] ExtractTagsFromConnectionString(string connectionString)
{
// Parse the connection string
var builder = new DbConnectionStringBuilder { ConnectionString = connectionString };
return new[]
{
new KeyValuePair<string, string>(
Tags.DbName,
GetConnectionStringValue(builder, "Database", "Initial Catalog", "InitialCatalog")),
new KeyValuePair<string, string>(
Tags.DbUser,
GetConnectionStringValue(builder, "User ID", "UserID")),
new KeyValuePair<string, string>(
Tags.OutHost,
GetConnectionStringValue(builder, "Server", "Data Source", "DataSource", "Network Address", "NetworkAddress", "Address", "Addr", "Host"))
};
}
private static string GetConnectionStringValue(DbConnectionStringBuilder builder, params string[] names)
{
foreach (string name in names)
{
if (builder.TryGetValue(name, out object valueObj) &&
valueObj is string value)
{
return value;
}
}
return null;
}
}
}

View File

@ -0,0 +1,97 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using Datadog.Trace.ExtensionMethods;
using Datadog.Trace.Util;
using Moq;
using Xunit;
namespace Datadog.Trace.Tests.ExtensionMethods
{
public class SpanExtensionsTests
{
public SpanExtensionsTests()
{
// Reset the cache
DbCommandCache.Cache = new ConcurrentDictionary<string, KeyValuePair<string, string>[]>();
}
[Theory]
[InlineData("Server=myServerName,myPortNumber;Database=myDataBase;User Id=myUsername;Password=myPassword;", "myDataBase", "myUsername", "myServerName,myPortNumber")]
[InlineData(@"Server=myServerName\myInstanceName;Database=myDataBase;User Id=myUsername;Password=myPassword;", "myDataBase", "myUsername", @"myServerName\myInstanceName")]
[InlineData(@"Server=.\SQLExpress;AttachDbFilename=|DataDirectory|mydbfile.mdf;Database=dbname;Trusted_Connection=Yes;", "dbname", null, @".\SQLExpress")]
public void ExtractProperTagsFromConnectionString(
string connectionString,
string expectedDbName,
string expectedUserId,
string expectedHost)
{
var spanContext = new SpanContext(Mock.Of<ISpanContext>(), Mock.Of<ITraceContext>(), "test");
var span = new Span(spanContext, null);
span.AddTagsFromDbCommand(CreateDbCommand(connectionString));
Assert.Equal(expectedDbName, span.GetTag(Tags.DbName));
Assert.Equal(expectedUserId, span.GetTag(Tags.DbUser));
Assert.Equal(expectedHost, span.GetTag(Tags.OutHost));
}
[Fact]
public void SetSpanTypeToSql()
{
const string connectionString = "Server=myServerName;Database=myDataBase;User Id=myUsername;Password=myPassword;";
const string commandText = "SELECT * FROM Table ORDER BY id";
var spanContext = new SpanContext(Mock.Of<ISpanContext>(), Mock.Of<ITraceContext>(), "test");
var span = new Span(spanContext, null);
span.AddTagsFromDbCommand(CreateDbCommand(connectionString, commandText));
Assert.Equal(SpanTypes.Sql, span.Type);
Assert.Equal(commandText, span.ResourceName);
}
[Fact]
public void ShouldDisableCacheIfTooManyConnectionStrings()
{
const string connectionStringTemplate = "Server=myServerName{0};Database=myDataBase;User Id=myUsername;Password=myPassword;";
var spanContext = new SpanContext(Mock.Of<ISpanContext>(), Mock.Of<ITraceContext>(), "test");
var span = new Span(spanContext, null);
// Fill-up the cache and test the logic with cache enabled
for (int i = 0; i <= DbCommandCache.MaxConnectionStrings; i++)
{
var connectionString = string.Format(connectionStringTemplate, i);
span.AddTagsFromDbCommand(CreateDbCommand(connectionString));
Assert.NotNull(DbCommandCache.Cache);
Assert.Equal("myServerName" + i, span.GetTag(Tags.OutHost));
}
// Test the logic with cache disabled
for (int i = 0; i <= 10; i++)
{
var connectionString = string.Format(connectionStringTemplate, "NoCache" + i);
span.AddTagsFromDbCommand(CreateDbCommand(connectionString));
Assert.Null(DbCommandCache.Cache);
Assert.Equal("myServerName" + "NoCache" + i, span.GetTag(Tags.OutHost));
}
}
private static IDbCommand CreateDbCommand(string connectionString, string commandText = null)
{
var dbConnection = new Mock<IDbConnection>();
dbConnection.SetupGet(c => c.ConnectionString).Returns(connectionString);
var dbCommand = new Mock<IDbCommand>();
dbCommand.SetupGet(c => c.Connection).Returns(dbConnection.Object);
dbCommand.SetupGet(c => c.CommandText).Returns(commandText);
return dbCommand.Object;
}
}
}