-
Notifications
You must be signed in to change notification settings - Fork 162
Expand file tree
/
Copy pathDbScopeFactory.cs
More file actions
375 lines (331 loc) · 18 KB
/
DbScopeFactory.cs
File metadata and controls
375 lines (331 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// <copyright file="DbScopeFactory.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>
#nullable enable
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Datadog.Trace.AppSec;
using Datadog.Trace.Configuration;
using Datadog.Trace.Configuration.Schema;
using Datadog.Trace.DatabaseMonitoring;
using Datadog.Trace.Logging;
using Datadog.Trace.Tagging;
using Datadog.Trace.Util;
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.AdoNet
{
internal static class DbScopeFactory
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(DbScopeFactory));
private static bool _dbCommandCachingLogged = false;
private static Scope? CreateDbCommandScope(Tracer tracer, IDbCommand command, IntegrationId integrationId, string dbType, string operationName, string serviceName, string? serviceNameSource, ref DbCommandCache.TagsCacheItem tagsFromConnectionString)
{
var perTraceSettings = tracer.CurrentTraceSettings;
if (!perTraceSettings.Settings.IsIntegrationEnabled(integrationId) || !perTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AdoNet))
{
// integration disabled, don't create a scope, skip this span
return null;
}
Scope? scope = null;
SqlTags tags;
var commandText = command.CommandText ?? string.Empty;
try
{
var parent = tracer.InternalActiveScope?.Span;
if (parent is { Type: SpanTypes.Sql } &&
HasDbType(parent, dbType) &&
(parent.ResourceName == commandText || commandText.StartsWith(DatabaseMonitoringPropagator.DbmPrefix) || commandText == DatabaseMonitoringPropagator.SetContextCommand))
{
// we are already instrumenting this,
// don't instrument nested methods that belong to the same stacktrace
// e.g. ExecuteReader() -> ExecuteReader(commandBehavior)
return null;
}
// We might block the SQL call from RASP depending on the query
VulnerabilitiesModule.OnSqlQuery(commandText, integrationId);
tags = perTraceSettings.Schema.Database.CreateSqlTags();
tags.DbType = dbType;
tags.InstrumentationName = IntegrationRegistry.GetName(integrationId);
tags.DbName = tagsFromConnectionString.DbName;
tags.DbUser = tagsFromConnectionString.DbUser;
tags.OutHost = tagsFromConnectionString.OutHost;
tags.SetAnalyticsSampleRate(integrationId, perTraceSettings.Settings, enabledWithGlobalSetting: false);
perTraceSettings.Schema.RemapPeerService(tags);
scope = tracer.StartActiveInternal(operationName, tags: tags, serviceName: serviceName, serviceNameSource: serviceNameSource);
scope.Span.ResourceName = commandText;
scope.Span.Type = SpanTypes.Sql;
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(integrationId);
}
catch (Exception ex) when (ex is not BlockException)
{
Log.Error(ex, "Error creating or populating scope.");
return scope;
}
try
{
if (tracer.Settings.DbmPropagationMode != DbmPropagationLevel.Disabled)
{
var alreadyInjected = commandText.StartsWith(DatabaseMonitoringPropagator.DbmPrefix) ||
// if we appended the comment, we need to look for a potential DBM comment in the whole string.
(DatabaseMonitoringPropagator.ShouldAppend(integrationId, commandText) && commandText.Contains(DatabaseMonitoringPropagator.DbmPrefix));
if (alreadyInjected)
{
// The command text is already injected, so they're probably caching the SqlCommand
// that's not a problem if they're using 'service' mode, but it _is_ a problem for 'full' mode
// There's not a lot we can do about it (we don't want to start parsing commandText), so just
// report it for now
if (!Volatile.Read(ref _dbCommandCachingLogged)
&& tracer.Settings.DbmPropagationMode != DbmPropagationLevel.Service)
{
_dbCommandCachingLogged = true;
var spanContext = scope.Span.Context;
Log.Warning(
"The {CommandType} IDbCommand instance already contains DBM information. Caching of the command objects is not supported with full DBM mode. [s_id: {SpanId}, t_id: {TraceId}]",
command.CommandType,
spanContext.RawSpanId,
spanContext.RawTraceId);
}
}
else
{
// PropagateDataViaComment (service) - this injects varius trace information as a comment in the query
// PropagateDataViaContext (full) - this makes a special set context_info for Microsoft SQL Server (nothing else supported)
var traceParentInjectedInComment = DatabaseMonitoringPropagator.PropagateDataViaComment(tracer.Settings.DbmPropagationMode, integrationId, command, tracer.DefaultServiceName, tagsFromConnectionString.DbName, tagsFromConnectionString.OutHost, scope.Span, tracer.Settings.InjectContextIntoStoredProceduresEnabled);
// try context injection only after comment injection, so that if it fails, we still have service level propagation
var traceParentInjectedInContext = DatabaseMonitoringPropagator.PropagateDataViaContext(tracer.Settings.DbmPropagationMode, integrationId, command, scope.Span);
if (traceParentInjectedInComment || traceParentInjectedInContext)
{
tags.DbmTraceInjected = "true";
}
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Error propagating span data for DBM");
}
// we have to start the span before doing the DBM propagation work (to have the span ID)
// but ultimately, we don't want to measure the time spent instrumenting.
scope.Span.ResetStartTime();
return scope;
static bool HasDbType(Span span, string dbType)
{
if (span.Tags is SqlTags sqlTags)
{
return sqlTags.DbType == dbType;
}
return span.GetTag(Tags.DbType) == dbType;
}
}
public static bool TryGetIntegrationDetails(
HashSet<string> disabledAdoNetCommandTypes,
string? commandTypeFullName,
[NotNullWhen(true)] out IntegrationId? integrationId,
[NotNullWhen(true)] out string? dbType)
{
// TODO: optimize this switch
switch (commandTypeFullName)
{
case null:
integrationId = null;
dbType = null;
return false;
case "System.Data.SqlClient.SqlCommand" or "Microsoft.Data.SqlClient.SqlCommand":
integrationId = IntegrationId.SqlClient;
dbType = DbType.SqlServer;
return true;
case "Npgsql.NpgsqlCommand":
integrationId = IntegrationId.Npgsql;
dbType = DbType.PostgreSql;
return true;
case "MySql.Data.MySqlClient.MySqlCommand" or "MySqlConnector.MySqlCommand":
integrationId = IntegrationId.MySql;
dbType = DbType.MySql;
return true;
case "Oracle.ManagedDataAccess.Client.OracleCommand" or "Oracle.DataAccess.Client.OracleCommand":
integrationId = IntegrationId.Oracle;
dbType = DbType.Oracle;
return true;
case "System.Data.SQLite.SQLiteCommand" or "Microsoft.Data.Sqlite.SqliteCommand":
// note capitalization in SQLite/Sqlite
integrationId = IntegrationId.Sqlite;
dbType = DbType.Sqlite;
return true;
default:
string commandTypeName = commandTypeFullName.Substring(commandTypeFullName.LastIndexOf(".", StringComparison.Ordinal) + 1);
if (IsDisabledCommandType(commandTypeName, disabledAdoNetCommandTypes))
{
integrationId = null;
dbType = null;
return false;
}
const string commandSuffix = "Command";
int lastIndex = commandTypeFullName.LastIndexOf(".", StringComparison.Ordinal);
string namespaceName = lastIndex > 0 ? commandTypeFullName.Substring(0, lastIndex) : string.Empty;
integrationId = IntegrationId.AdoNet;
dbType = commandTypeName switch
{
_ when namespaceName.Length == 0 && commandTypeName == commandSuffix => "command",
_ when namespaceName.Contains(".") && commandTypeName == commandSuffix =>
// the + 1 could be dangerous and cause IndexOutOfRangeException, but this shouldn't happen
// a period should never be the last character in a namespace
namespaceName.Substring(namespaceName.LastIndexOf('.') + 1).ToLowerInvariant(),
_ when commandTypeName == commandSuffix =>
namespaceName.ToLowerInvariant(),
_ when commandTypeName.EndsWith(commandSuffix) =>
commandTypeName.Substring(0, commandTypeName.Length - commandSuffix.Length).ToLowerInvariant(),
_ => commandTypeName.ToLowerInvariant()
};
return true;
}
}
internal static bool IsDisabledCommandType(string commandTypeName, HashSet<string> disabledAdoNetCommandTypes)
{
if (string.IsNullOrEmpty(commandTypeName))
{
return false;
}
var disabledTypes = disabledAdoNetCommandTypes;
if (disabledTypes is null || disabledTypes.Count == 0)
{
return false;
}
foreach (var disabledType in disabledTypes)
{
if (string.Equals(disabledType, commandTypeName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public static class Cache<TCommand>
{
// ReSharper disable StaticMemberInGenericType
// Static fields used intentionally to cache a different set of values for each TCommand.
private static readonly Type CommandType;
private static readonly string? DbTypeName;
private static readonly string? OperationName;
private static readonly IntegrationId IntegrationId;
// ServiceName cache
private static KeyValuePair<string, ServiceNameMetadata> _serviceNameCache;
// ConnectionString tags cache
private static KeyValuePair<string, DbCommandCache.TagsCacheItem> _tagsByConnectionStringCache;
// ReSharper restore StaticMemberInGenericType
static Cache()
{
CommandType = typeof(TCommand);
if (TryGetIntegrationDetails(Tracer.Instance.Settings.DisabledAdoNetCommandTypes, CommandType.FullName, out var integrationId, out var dbTypeName))
{
// cache values for this TCommand type
DbTypeName = dbTypeName;
OperationName = $"{DbTypeName}.query";
IntegrationId = integrationId.Value;
}
}
public static Scope? CreateDbCommandScope(Tracer tracer, IDbCommand command)
{
var commandType = command.GetType();
if (commandType == CommandType && DbTypeName is not null && OperationName is not null)
{
// use the cached values if command.GetType() == typeof(TCommand)
// and we successfully called TryGetIntegrationDetails() in the ctor
var tagsFromConnectionString = GetTagsFromConnectionString(command);
var (cachedServiceName, cachedServiceNameSource) = GetServiceNameMetadata(tracer, DbTypeName);
return DbScopeFactory.CreateDbCommandScope(
tracer: tracer,
command: command,
integrationId: IntegrationId,
dbType: DbTypeName,
operationName: OperationName,
serviceName: cachedServiceName,
serviceNameSource: cachedServiceNameSource,
tagsFromConnectionString: ref tagsFromConnectionString);
}
// if command.GetType() != typeof(TCommand), we are probably instrumenting a method
// defined in a base class like DbCommand and we can't use the cached values
if (TryGetIntegrationDetails(tracer.Settings.DisabledAdoNetCommandTypes, commandType.FullName, out var integrationId, out var dbTypeName))
{
var operationName = $"{dbTypeName}.query";
var tagsFromConnectionString = GetTagsFromConnectionString(command);
var (resolvedServiceName, resolvedServiceNameSource) = GetServiceNameMetadata(tracer, dbTypeName);
return DbScopeFactory.CreateDbCommandScope(
tracer: tracer,
command: command,
integrationId: integrationId.Value,
dbType: dbTypeName,
operationName: operationName,
serviceName: resolvedServiceName,
serviceNameSource: resolvedServiceNameSource,
tagsFromConnectionString: ref tagsFromConnectionString);
}
return null;
}
private static ServiceNameMetadata GetServiceNameMetadata(Tracer tracer, string dbTypeName)
{
if (tracer.CurrentTraceSettings.ServiceNames.TryGetValue(dbTypeName, out var serviceName))
{
return new ServiceNameMetadata(serviceName, ServiceNameMetadata.OptServiceMapping);
}
if (DbTypeName != dbTypeName)
{
// We cannot cache in the base class
return tracer.CurrentTraceSettings.GetServiceNameMetadata(dbTypeName);
}
var serviceNameCache = _serviceNameCache;
// If not a base class
if (serviceNameCache.Key == tracer.DefaultServiceName)
{
// Service has not changed
// Fastpath
return serviceNameCache.Value;
}
// We create or replace the cache with the new service name
// Slowpath
var defaultServiceName = tracer.DefaultServiceName;
var metadata = tracer.CurrentTraceSettings.GetServiceNameMetadata(dbTypeName);
_serviceNameCache = new KeyValuePair<string, ServiceNameMetadata>(defaultServiceName, metadata);
return metadata;
}
private static DbCommandCache.TagsCacheItem GetTagsFromConnectionString(IDbCommand command)
{
string? connectionString = null;
try
{
if (command.GetType().FullName == "System.Data.Common.DbDataSource.DbCommandWrapper")
{
return default;
}
connectionString = command.Connection?.ConnectionString;
}
catch (NotSupportedException nsException)
{
Log.Debug(nsException, "ConnectionString cannot be retrieved from the command.");
}
catch (Exception ex)
{
Log.Debug(ex, "Error trying to retrieve the ConnectionString from the command.");
}
if (connectionString is null)
{
return default;
}
// Check if the connection string is the one in the cache
var tagsByConnectionString = _tagsByConnectionStringCache;
if (tagsByConnectionString.Key == connectionString)
{
// Fastpath
return tagsByConnectionString.Value;
}
// Cache the new tags by connection string
// Slowpath
var tags = DbCommandCache.GetTagsFromDbCommand(command);
_tagsByConnectionStringCache = new KeyValuePair<string, DbCommandCache.TagsCacheItem>(connectionString, tags);
return tags;
}
}
}
}