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:
parent
6a1abe0a45
commit
382ea15cb0
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue