diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 10633975fd4d..4898fe1995fd 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -180,11 +180,11 @@ - - - - - + + + + + @@ -373,12 +373,12 @@ - - - - - - + + + + + + diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md index 345edc891f42..8968c75a111c 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/CHANGELOG.md @@ -10,6 +10,21 @@ ### Other Changes +* Update OpenTelemetry dependencies + ([#48574](https://github.com/Azure/azure-sdk-for-net/pull/48574)) + - OpenTelemetry 1.11.2 + - OpenTelemetry.Extensions.Hosting 1.11.2 + - OpenTelemetry.Instrumentation.AspNetCore 1.11.1 + - OpenTelemetry.Instrumentation.Http 1.11.1 + +* Updated the code of vendored instrumentation library `OpenTelemetry.Instrumentation.SqlClient` from the OpenTelemetry .NET contrib repository. + Code has been updated to [1.11.0-beta.2](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/Instrumentation.SqlClient-1.11.0-beta.2/src/OpenTelemetry.Instrumentation.SqlClient). + ([#48608](https://github.com/Azure/azure-sdk-for-net/pull/48608)) + +* Updated the code of vendored resource detector library `OpenTelemetry.Resources.Azure` from the OpenTelemetry .NET contrib repository. + Code has been updated to [1.11.0-beta.2](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/Resources.Azure-1.11.0-beta.2/src/OpenTelemetry.Resources.Azure). + ([#48608](https://github.com/Azure/azure-sdk-for-net/pull/48608)) + ## 1.3.0-beta.2 (2024-10-11) ### Bugs Fixed diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj index d377f9c299b1..45441973b959 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Azure.Monitor.OpenTelemetry.AspNetCore.csproj @@ -23,10 +23,10 @@ - + - + diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs index f089caf08d9b..b1ad4815ab36 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Reflection; using OpenTelemetry.Internal; using OpenTelemetry.Trace; @@ -20,10 +21,103 @@ internal sealed class SqlActivitySourceHelper public static readonly AssemblyName AssemblyName = Assembly.GetName(); public static readonly string ActivitySourceName = AssemblyName.Name!; public static readonly ActivitySource ActivitySource = new(ActivitySourceName, Assembly.GetPackageVersion()); - public static readonly string ActivityName = ActivitySourceName + ".Execute"; - public static readonly IEnumerable> CreationTags = new[] + public static readonly string MeterName = AssemblyName.Name!; + public static readonly Meter Meter = new(MeterName, Assembly.GetPackageVersion()); + + public static readonly Histogram DbClientOperationDuration = Meter.CreateHistogram( + "db.client.operation.duration", + unit: "s", + description: "Duration of database client operations.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] }); + + internal static readonly string[] SharedTagNames = + [ + SemanticConventions.AttributeDbSystem, + SemanticConventions.AttributeDbCollectionName, + SemanticConventions.AttributeDbNamespace, + SemanticConventions.AttributeDbResponseStatusCode, + SemanticConventions.AttributeDbOperationName, + SemanticConventions.AttributeErrorType, + SemanticConventions.AttributeServerPort, + SemanticConventions.AttributeServerAddress, + ]; + + public static TagList GetTagListFromConnectionInfo(string? dataSource, string? databaseName, SqlClientTraceInstrumentationOptions options, out string activityName) + { + activityName = MicrosoftSqlServerDatabaseSystemName; + + var tags = new TagList + { + { SemanticConventions.AttributeDbSystem, MicrosoftSqlServerDatabaseSystemName }, + }; + + if (dataSource != null) + { + var connectionDetails = SqlConnectionDetails.ParseFromDataSource(dataSource); + + if (options.EmitOldAttributes && !string.IsNullOrEmpty(databaseName)) + { + tags.Add(SemanticConventions.AttributeDbName, databaseName); + activityName = databaseName!; + } + + if (options.EmitNewAttributes && !string.IsNullOrEmpty(databaseName)) + { + var dbNamespace = !string.IsNullOrEmpty(connectionDetails.InstanceName) + ? $"{connectionDetails.InstanceName}.{databaseName}" // TODO: Refactor SqlConnectionDetails to include database to avoid string allocation here. + : databaseName!; + tags.Add(SemanticConventions.AttributeDbNamespace, dbNamespace); + activityName = dbNamespace; + } + + var serverAddress = connectionDetails.ServerHostName ?? connectionDetails.ServerIpAddress; + if (!string.IsNullOrEmpty(serverAddress)) + { + tags.Add(SemanticConventions.AttributeServerAddress, serverAddress); + if (connectionDetails.Port.HasValue) + { + tags.Add(SemanticConventions.AttributeServerPort, connectionDetails.Port); + } + + if (activityName == MicrosoftSqlServerDatabaseSystemName) + { + activityName = connectionDetails.Port.HasValue + ? $"{serverAddress}:{connectionDetails.Port}" // TODO: Another opportunity to refactor SqlConnectionDetails + : serverAddress!; + } + } + + if (options.EmitOldAttributes && !string.IsNullOrEmpty(connectionDetails.InstanceName)) + { + tags.Add(SemanticConventions.AttributeDbMsSqlInstanceName, connectionDetails.InstanceName); + } + } + else if (!string.IsNullOrEmpty(databaseName)) + { + if (options.EmitNewAttributes) + { + tags.Add(SemanticConventions.AttributeDbNamespace, databaseName); + } + + if (options.EmitOldAttributes) + { + tags.Add(SemanticConventions.AttributeDbName, databaseName); + } + + activityName = databaseName!; + } + + return tags; + } + + internal static double CalculateDurationFromTimestamp(long begin, long? end = null) { - new KeyValuePair(SemanticConventions.AttributeDbSystem, MicrosoftSqlServerDatabaseSystemName), - }; + end ??= Stopwatch.GetTimestamp(); + var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + var delta = end - begin; + var ticks = (long)(timestampToTicks * delta); + var duration = new TimeSpan(ticks); + return duration.TotalSeconds; + } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs index bf3b77c5999a..c990695a5434 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs @@ -4,14 +4,15 @@ #if !NETFRAMEWORK using System.Data; using System.Diagnostics; -using OpenTelemetry.Trace; -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif +using System.Globalization; +using OpenTelemetry.Trace; namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; -#if NET6_0_OR_GREATER +#if NET [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] #endif internal sealed class SqlClientDiagnosticListener : ListenerHandler @@ -27,47 +28,57 @@ internal sealed class SqlClientDiagnosticListener : ListenerHandler private readonly PropertyFetcher commandFetcher = new("Command"); private readonly PropertyFetcher connectionFetcher = new("Connection"); - private readonly PropertyFetcher dataSourceFetcher = new("DataSource"); - private readonly PropertyFetcher databaseFetcher = new("Database"); + private readonly PropertyFetcher dataSourceFetcher = new("DataSource"); + private readonly PropertyFetcher databaseFetcher = new("Database"); private readonly PropertyFetcher commandTypeFetcher = new("CommandType"); - private readonly PropertyFetcher commandTextFetcher = new("CommandText"); + private readonly PropertyFetcher commandTextFetcher = new("CommandText"); private readonly PropertyFetcher exceptionFetcher = new("Exception"); - private readonly SqlClientTraceInstrumentationOptions options; + private readonly PropertyFetcher exceptionNumberFetcher = new("Number"); + private readonly AsyncLocal beginTimestamp = new(); - public SqlClientDiagnosticListener(string sourceName, SqlClientTraceInstrumentationOptions? options) + public SqlClientDiagnosticListener(string sourceName) : base(sourceName) { - this.options = options ?? new SqlClientTraceInstrumentationOptions(); } public override bool SupportsNullActivity => true; public override void OnEventWritten(string name, object? payload) { + if (SqlClientInstrumentation.TracingHandles == 0 && SqlClientInstrumentation.MetricHandles == 0) + { + return; + } + + var options = SqlClientInstrumentation.TracingOptions; var activity = Activity.Current; switch (name) { case SqlDataBeforeExecuteCommand: case SqlMicrosoftBeforeExecuteCommand: { - // SqlClient does not create an Activity. So the activity coming in here will be null or the root span. + _ = this.commandFetcher.TryFetch(payload, out var command); + if (command == null) + { + SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); + return; + } + + _ = this.connectionFetcher.TryFetch(command, out var connection); + _ = this.databaseFetcher.TryFetch(connection, out var databaseName); + _ = this.dataSourceFetcher.TryFetch(connection, out var dataSource); + + var startTags = SqlActivitySourceHelper.GetTagListFromConnectionInfo(dataSource, databaseName, options, out var activityName); activity = SqlActivitySourceHelper.ActivitySource.StartActivity( - SqlActivitySourceHelper.ActivityName, + activityName, ActivityKind.Client, default(ActivityContext), - SqlActivitySourceHelper.CreationTags); + startTags); if (activity == null) { // There is no listener or it decided not to sample the current request. - return; - } - - _ = this.commandFetcher.TryFetch(payload, out var command); - if (command == null) - { - SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); - activity.Stop(); + this.beginTimestamp.Value = Stopwatch.GetTimestamp(); return; } @@ -75,7 +86,7 @@ public override void OnEventWritten(string name, object? payload) { try { - if (this.options.Filter?.Invoke(command) == false) + if (options.Filter?.Invoke(command) == false) { SqlClientInstrumentationEventSource.Log.CommandIsFilteredOut(activity.OperationName); activity.IsAllDataRequested = false; @@ -91,51 +102,53 @@ public override void OnEventWritten(string name, object? payload) return; } - _ = this.connectionFetcher.TryFetch(command, out var connection); - _ = this.databaseFetcher.TryFetch(connection, out var database); - - if (database != null) - { - activity.DisplayName = (string)database; - activity.SetTag(SemanticConventions.AttributeDbName, database); - } - - _ = this.dataSourceFetcher.TryFetch(connection, out var dataSource); - _ = this.commandTextFetcher.TryFetch(command, out var commandText); - - if (dataSource != null) - { - this.options.AddConnectionLevelDetailsToActivity((string)dataSource, activity); - } - - if (this.commandTypeFetcher.TryFetch(command, out CommandType commandType)) + if (this.commandTypeFetcher.TryFetch(command, out var commandType) && + this.commandTextFetcher.TryFetch(command, out var commandText)) { switch (commandType) { case CommandType.StoredProcedure: - if (this.options.SetDbStatementForStoredProcedure) + if (options.EmitOldAttributes) { activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); } + if (options.EmitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbOperationName, "EXECUTE"); + activity.SetTag(SemanticConventions.AttributeDbCollectionName, commandText); + activity.SetTag(SemanticConventions.AttributeDbQueryText, commandText); + } + break; case CommandType.Text: - if (this.options.SetDbStatementForText) + if (options.SetDbStatementForText) { - activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); + var sanitizedSql = SqlProcessor.GetSanitizedSql(commandText); + if (options.EmitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, sanitizedSql); + } + + if (options.EmitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbQueryText, sanitizedSql); + } } break; case CommandType.TableDirect: break; + default: + break; } } try { - this.options.Enrich?.Invoke(activity, "OnCustom", command); + options.Enrich?.Invoke(activity, "OnCustom", command); } catch (Exception ex) { @@ -151,15 +164,18 @@ public override void OnEventWritten(string name, object? payload) if (activity == null) { SqlClientInstrumentationEventSource.Log.NullActivity(name); + this.RecordDuration(null, payload); return; } if (activity.Source != SqlActivitySourceHelper.ActivitySource) { + this.RecordDuration(null, payload); return; } activity.Stop(); + this.RecordDuration(activity, payload); } break; @@ -169,11 +185,13 @@ public override void OnEventWritten(string name, object? payload) if (activity == null) { SqlClientInstrumentationEventSource.Log.NullActivity(name); + this.RecordDuration(null, payload); return; } if (activity.Source != SqlActivitySourceHelper.ActivitySource) { + this.RecordDuration(null, payload); return; } @@ -181,13 +199,20 @@ public override void OnEventWritten(string name, object? payload) { if (activity.IsAllDataRequested) { - if (this.exceptionFetcher.TryFetch(payload, out Exception? exception) && exception != null) + if (this.exceptionFetcher.TryFetch(payload, out var exception) && exception != null) { + activity.AddTag(SemanticConventions.AttributeErrorType, exception.GetType().FullName); + + if (this.exceptionNumberFetcher.TryFetch(exception, out var exceptionNumber)) + { + activity.AddTag(SemanticConventions.AttributeDbResponseStatusCode, exceptionNumber.ToString(CultureInfo.InvariantCulture)); + } + activity.SetStatus(ActivityStatusCode.Error, exception.Message); - if (this.options.RecordException) + if (options.RecordException) { - activity.RecordException(exception); + activity.AddException(exception); } } else @@ -199,11 +224,83 @@ public override void OnEventWritten(string name, object? payload) finally { activity.Stop(); + this.RecordDuration(activity, payload, hasError: true); } } break; + default: + break; + } + } + + private void RecordDuration(Activity? activity, object? payload, bool hasError = false) + { + if (SqlClientInstrumentation.MetricHandles == 0) + { + return; + } + + var tags = default(TagList); + + if (activity != null && activity.IsAllDataRequested) + { + foreach (var name in SqlActivitySourceHelper.SharedTagNames) + { + var value = activity.GetTagItem(name); + if (value != null) + { + tags.Add(name, value); + } + } + } + else if (payload != null) + { + if (this.commandFetcher.TryFetch(payload, out var command) && command != null && + this.connectionFetcher.TryFetch(command, out var connection)) + { + this.databaseFetcher.TryFetch(connection, out var databaseName); + this.dataSourceFetcher.TryFetch(connection, out var dataSource); + + var connectionTags = SqlActivitySourceHelper.GetTagListFromConnectionInfo( + dataSource, + databaseName, + SqlClientInstrumentation.TracingOptions, + out _); + + foreach (var tag in connectionTags) + { + tags.Add(tag.Key, tag.Value); + } + + if (this.commandTypeFetcher.TryFetch(command, out var commandType) && + commandType == CommandType.StoredProcedure) + { + if (this.commandTextFetcher.TryFetch(command, out var commandText)) + { + tags.Add(SemanticConventions.AttributeDbOperationName, "EXECUTE"); + tags.Add(SemanticConventions.AttributeDbCollectionName, commandText); + } + } + } + + if (hasError) + { + if (this.exceptionFetcher.TryFetch(payload, out var exception) && exception != null) + { + tags.Add(SemanticConventions.AttributeErrorType, exception.GetType().FullName); + + if (this.exceptionNumberFetcher.TryFetch(exception, out var exceptionNumber)) + { + tags.Add(SemanticConventions.AttributeDbResponseStatusCode, exceptionNumber.ToString(CultureInfo.InvariantCulture)); + } + } + } } + + var duration = activity?.Duration.TotalSeconds + ?? SqlActivitySourceHelper.CalculateDurationFromTimestamp(this.beginTimestamp.Value); + SqlActivitySourceHelper.DbClientOperationDuration.Record(duration, tags); } } #endif diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlConnectionDetails.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlConnectionDetails.cs new file mode 100644 index 000000000000..78110fd1cd70 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlConnectionDetails.cs @@ -0,0 +1,130 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Text.RegularExpressions; + +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +internal sealed class SqlConnectionDetails +{ + /* + * Match... + * protocol[ ]:[ ]serverName + * serverName + * serverName[ ]\[ ]instanceName + * serverName[ ],[ ]port + * serverName[ ]\[ ]instanceName[ ],[ ]port + * + * [ ] can be any number of white-space, SQL allows it for some reason. + * + * Optional "protocol" can be "tcp", "lpc" (shared memory), or "np" (named pipes). See: + * https://docs.microsoft.com/troubleshoot/sql/connect/use-server-name-parameter-connection-string, and + * https://docs.microsoft.com/dotnet/api/system.data.sqlclient.sqlconnection.connectionstring?view=dotnet-plat-ext-5.0 + * + * In case of named pipes the Data Source string can take form of: + * np:serverName\instanceName, or + * np:\\serverName\pipe\pipeName, or + * np:\\serverName\pipe\MSSQL$instanceName\pipeName - in this case a separate regex (see NamedPipeRegex below) + * is used to extract instanceName + */ + private static readonly Regex DataSourceRegex = new("^(.*\\s*:\\s*\\\\{0,2})?(.*?)\\s*(?:[\\\\,]|$)\\s*(.*?)\\s*(?:,|$)\\s*(.*)$", RegexOptions.Compiled); + + /// + /// In a Data Source string like "np:\\serverName\pipe\MSSQL$instanceName\pipeName" match the + /// "pipe\MSSQL$instanceName" segment to extract instanceName if it is available. + /// + /// + /// + /// + private static readonly Regex NamedPipeRegex = new("pipe\\\\MSSQL\\$(.*?)\\\\", RegexOptions.Compiled); + + private static readonly ConcurrentDictionary ConnectionDetailCache = new(StringComparer.OrdinalIgnoreCase); + + private SqlConnectionDetails() + { + } + + public string? ServerHostName { get; private set; } + + public string? ServerIpAddress { get; private set; } + + public string? InstanceName { get; private set; } + + public int? Port { get; private set; } + + public static SqlConnectionDetails ParseFromDataSource(string dataSource) + { + if (ConnectionDetailCache.TryGetValue(dataSource, out var connectionDetails)) + { + return connectionDetails; + } + + var match = DataSourceRegex.Match(dataSource); + + var serverHostName = match.Groups[2].Value; + string? serverIpAddress = null; + string? instanceName = null; + int? port = null; + + var uriHostNameType = Uri.CheckHostName(serverHostName); + if (uriHostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6) + { + serverIpAddress = serverHostName; + serverHostName = null; + } + + var maybeProtocol = match.Groups[1].Value; + var isNamedPipe = maybeProtocol.Length > 0 && + maybeProtocol.StartsWith("np", StringComparison.OrdinalIgnoreCase); + + if (isNamedPipe) + { + var pipeName = match.Groups[3].Value; + if (pipeName.Length > 0) + { + var namedInstancePipeMatch = NamedPipeRegex.Match(pipeName); + if (namedInstancePipeMatch.Success) + { + instanceName = namedInstancePipeMatch.Groups[1].Value; + } + } + } + else + { + if (match.Groups[4].Length > 0) + { + instanceName = match.Groups[3].Value; + port = int.TryParse(match.Groups[4].Value, out var parsedPort) + ? parsedPort == 1433 ? null : parsedPort + : null; + } + else if (int.TryParse(match.Groups[3].Value, out var parsedPort)) + { + instanceName = null; + port = parsedPort == 1433 ? null : parsedPort; + } + else + { + instanceName = match.Groups[3].Value; + if (string.IsNullOrEmpty(instanceName)) + { + instanceName = null; + } + + port = null; + } + } + + connectionDetails = new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = instanceName, + Port = port, + }; + + ConnectionDetailCache.TryAdd(dataSource, connectionDetails); + return connectionDetails; + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs index 40c3c056a898..f58f97cd2d5b 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs @@ -29,15 +29,10 @@ internal sealed class SqlEventSourceListener : EventListener internal const int BeginExecuteEventId = 1; internal const int EndExecuteEventId = 2; - private readonly SqlClientTraceInstrumentationOptions options; + private readonly AsyncLocal beginTimestamp = new(); private EventSource? adoNetEventSource; private EventSource? mdsEventSource; - public SqlEventSourceListener(SqlClientTraceInstrumentationOptions? options = null) - { - this.options = options ?? new SqlClientTraceInstrumentationOptions(); - } - public override void Dispose() { if (this.adoNetEventSource != null) @@ -88,6 +83,29 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) } } + private static (bool HasError, string? ErrorNumber, string? ExceptionType) ExtractErrorFromEvent(EventWrittenEventArgs eventData) + { + var compositeState = (int)eventData.Payload[1]; + + if ((compositeState & 0b001) != 0b001) + { + if ((compositeState & 0b010) == 0b010) + { + var errorNumber = $"{eventData.Payload[2]}"; + var exceptionType = eventData.EventSource.Name == MdsEventSourceName + ? "Microsoft.Data.SqlClient.SqlException" + : "System.Data.SqlClient.SqlException"; + return (true, errorNumber, exceptionType); + } + else + { + return (true, null, null); + } + } + + return (false, null, null); + } + private void OnBeginExecute(EventWrittenEventArgs eventData) { /* @@ -106,38 +124,49 @@ private void OnBeginExecute(EventWrittenEventArgs eventData) (https://github.com/dotnet/SqlClient/blob/f4568ce68da21db3fe88c0e72e1287368aaa1dc8/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6641) */ + if (SqlClientInstrumentation.TracingHandles == 0 && SqlClientInstrumentation.MetricHandles == 0) + { + return; + } + + var options = SqlClientInstrumentation.TracingOptions; + if (eventData.Payload.Count < 4) { SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnBeginExecute)); return; } + var dataSource = (string)eventData.Payload[1]; + var databaseName = (string)eventData.Payload[2]; + var startTags = SqlActivitySourceHelper.GetTagListFromConnectionInfo(dataSource, databaseName, options, out var activityName); var activity = SqlActivitySourceHelper.ActivitySource.StartActivity( - SqlActivitySourceHelper.ActivityName, + activityName, ActivityKind.Client, default(ActivityContext), - SqlActivitySourceHelper.CreationTags); + startTags); if (activity == null) { // There is no listener or it decided not to sample the current request. + this.beginTimestamp.Value = Stopwatch.GetTimestamp(); return; } - string? databaseName = (string)eventData.Payload[2]; - - activity.DisplayName = databaseName; - if (activity.IsAllDataRequested) { - activity.SetTag(SemanticConventions.AttributeDbName, databaseName); - - this.options.AddConnectionLevelDetailsToActivity((string)eventData.Payload[1], activity); - - string commandText = (string)eventData.Payload[3]; - if (!string.IsNullOrEmpty(commandText) && this.options.SetDbStatementForText) + var commandText = (string)eventData.Payload[3]; + if (!string.IsNullOrEmpty(commandText) && options.SetDbStatementForText) { - activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); + if (options.EmitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); + } + + if (options.EmitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbQueryText, commandText); + } } } } @@ -151,12 +180,23 @@ private void OnEndExecute(EventWrittenEventArgs eventData) [2] -> SqlExceptionNumber */ + if (SqlClientInstrumentation.TracingHandles == 0 && SqlClientInstrumentation.MetricHandles == 0) + { + return; + } + if (eventData.Payload.Count < 3) { SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnEndExecute)); return; } + if (SqlClientInstrumentation.TracingHandles == 0 && SqlClientInstrumentation.MetricHandles != 0) + { + this.RecordDuration(null, eventData); + return; + } + var activity = Activity.Current; if (activity?.Source != SqlActivitySourceHelper.ActivitySource) { @@ -167,13 +207,15 @@ private void OnEndExecute(EventWrittenEventArgs eventData) { if (activity.IsAllDataRequested) { - int compositeState = (int)eventData.Payload[1]; - if ((compositeState & 0b001) != 0b001) + var (hasError, errorNumber, exceptionType) = ExtractErrorFromEvent(eventData); + + if (hasError) { - if ((compositeState & 0b010) == 0b010) + if (errorNumber != null && exceptionType != null) { - var errorText = $"SqlExceptionNumber {eventData.Payload[2]} thrown."; - activity.SetStatus(ActivityStatusCode.Error, errorText); + activity.SetStatus(ActivityStatusCode.Error, errorNumber); + activity.SetTag(SemanticConventions.AttributeDbResponseStatusCode, errorNumber); + activity.SetTag(SemanticConventions.AttributeErrorType, exceptionType); } else { @@ -185,7 +227,49 @@ private void OnEndExecute(EventWrittenEventArgs eventData) finally { activity.Stop(); + this.RecordDuration(activity, eventData); + } + } + + private void RecordDuration(Activity? activity, EventWrittenEventArgs eventData) + { + if (SqlClientInstrumentation.MetricHandles == 0) + { + return; + } + + var tags = default(TagList); + + if (activity != null && activity.IsAllDataRequested) + { + foreach (var name in SqlActivitySourceHelper.SharedTagNames) + { + var value = activity.GetTagItem(name); + if (value != null) + { + tags.Add(name, value); + } + } } + else + { + tags.Add(SemanticConventions.AttributeDbSystem, SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName); + + var (hasError, errorNumber, exceptionType) = ExtractErrorFromEvent(eventData); + + if (hasError) + { + if (errorNumber != null && exceptionType != null) + { + tags.Add(SemanticConventions.AttributeDbResponseStatusCode, errorNumber); + tags.Add(SemanticConventions.AttributeErrorType, exceptionType); + } + } + } + + var duration = activity?.Duration.TotalSeconds + ?? SqlActivitySourceHelper.CalculateDurationFromTimestamp(this.beginTimestamp.Value); + SqlActivitySourceHelper.DbClientOperationDuration.Record(duration, tags); } } #endif diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs index dfd02f558ca4..e21f0bca88fd 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using OpenTelemetry.Instrumentation.SqlClient.Implementation; @@ -11,24 +11,31 @@ namespace OpenTelemetry.Instrumentation.SqlClient; /// /// SqlClient instrumentation. /// +#if NET +[RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)] +#endif internal sealed class SqlClientInstrumentation : IDisposable { + public static readonly SqlClientInstrumentation Instance = new SqlClientInstrumentation(); + internal const string SqlClientDiagnosticListenerName = "SqlClientDiagnosticListener"; -#if NET6_0_OR_GREATER +#if NET internal const string SqlClientTrimmingUnsupportedMessage = "Trimming is not yet supported with SqlClient instrumentation."; #endif + internal static int MetricHandles; + internal static int TracingHandles; #if NETFRAMEWORK private readonly SqlEventSourceListener sqlEventSourceListener; #else - private static readonly HashSet DiagnosticSourceEvents = new() - { + private static readonly HashSet DiagnosticSourceEvents = + [ "System.Data.SqlClient.WriteCommandBefore", "Microsoft.Data.SqlClient.WriteCommandBefore", "System.Data.SqlClient.WriteCommandAfter", "Microsoft.Data.SqlClient.WriteCommandAfter", "System.Data.SqlClient.WriteCommandError", - "Microsoft.Data.SqlClient.WriteCommandError", - }; + "Microsoft.Data.SqlClient.WriteCommandError" + ]; private readonly Func isEnabled = (eventName, _, _) => DiagnosticSourceEvents.Contains(eventName); @@ -39,18 +46,13 @@ internal sealed class SqlClientInstrumentation : IDisposable /// /// Initializes a new instance of the class. /// - /// Configuration options for sql instrumentation. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)] -#endif - public SqlClientInstrumentation( - SqlClientTraceInstrumentationOptions? options = null) + private SqlClientInstrumentation() { #if NETFRAMEWORK - this.sqlEventSourceListener = new SqlEventSourceListener(options); + this.sqlEventSourceListener = new SqlEventSourceListener(); #else this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( - name => new SqlClientDiagnosticListener(name, options), + name => new SqlClientDiagnosticListener(name), listener => listener.Name == SqlClientDiagnosticListenerName, this.isEnabled, SqlClientInstrumentationEventSource.Log.UnknownErrorProcessingEvent); @@ -58,6 +60,12 @@ public SqlClientInstrumentation( #endif } + public static SqlClientTraceInstrumentationOptions TracingOptions { get; set; } = new SqlClientTraceInstrumentationOptions(); + + public static IDisposable AddMetricHandle() => new MetricHandle(); + + public static IDisposable AddTracingHandle() => new TracingHandle(); + /// public void Dispose() { @@ -67,4 +75,48 @@ public void Dispose() this.diagnosticSourceSubscriber?.Dispose(); #endif } + +#if NET + [RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)] +#endif + private sealed class MetricHandle : IDisposable + { + private bool disposed; + + public MetricHandle() + { + Interlocked.Increment(ref MetricHandles); + } + + public void Dispose() + { + if (!this.disposed) + { + Interlocked.Decrement(ref MetricHandles); + this.disposed = true; + } + } + } + +#if NET + [RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)] +#endif + private sealed class TracingHandle : IDisposable + { + private bool disposed; + + public TracingHandle() + { + Interlocked.Increment(ref TracingHandles); + } + + public void Dispose() + { + if (!this.disposed) + { + Interlocked.Decrement(ref TracingHandles); + this.disposed = true; + } + } + } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientMeterProviderBuilderExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientMeterProviderBuilderExtensions.cs new file mode 100644 index 000000000000..560ded1543cb --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientMeterProviderBuilderExtensions.cs @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET +using System.Diagnostics.CodeAnalysis; +#endif +using OpenTelemetry.Instrumentation.SqlClient; +using OpenTelemetry.Instrumentation.SqlClient.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of dependency instrumentation. +/// +internal static class SqlClientMeterProviderBuilderExtensions +{ + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. +#if NET + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif + public static MeterProviderBuilder AddSqlClientInstrumentation(this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + + builder.AddInstrumentation(sp => + { + return SqlClientInstrumentation.AddMetricHandle(); + }); + + builder.AddMeter(SqlActivitySourceHelper.MeterName); + + return builder; + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs index 4cd58c6a6ae2..8fa4ab4b3bce 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs @@ -1,11 +1,11 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Collections.Concurrent; using System.Data; using System.Diagnostics; -using System.Text.RegularExpressions; +using Microsoft.Extensions.Configuration; using OpenTelemetry.Trace; +using static OpenTelemetry.Internal.DatabaseSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.SqlClient; @@ -17,51 +17,20 @@ namespace OpenTelemetry.Instrumentation.SqlClient; /// internal class SqlClientTraceInstrumentationOptions { - /* - * Match... - * protocol[ ]:[ ]serverName - * serverName - * serverName[ ]\[ ]instanceName - * serverName[ ],[ ]port - * serverName[ ]\[ ]instanceName[ ],[ ]port - * - * [ ] can be any number of white-space, SQL allows it for some reason. - * - * Optional "protocol" can be "tcp", "lpc" (shared memory), or "np" (named pipes). See: - * https://docs.microsoft.com/troubleshoot/sql/connect/use-server-name-parameter-connection-string, and - * https://docs.microsoft.com/dotnet/api/system.data.sqlclient.sqlconnection.connectionstring?view=dotnet-plat-ext-5.0 - * - * In case of named pipes the Data Source string can take form of: - * np:serverName\instanceName, or - * np:\\serverName\pipe\pipeName, or - * np:\\serverName\pipe\MSSQL$instanceName\pipeName - in this case a separate regex (see NamedPipeRegex below) - * is used to extract instanceName - */ - private static readonly Regex DataSourceRegex = new("^(.*\\s*:\\s*\\\\{0,2})?(.*?)\\s*(?:[\\\\,]|$)\\s*(.*?)\\s*(?:,|$)\\s*(.*)$", RegexOptions.Compiled); - /// - /// In a Data Source string like "np:\\serverName\pipe\MSSQL$instanceName\pipeName" match the - /// "pipe\MSSQL$instanceName" segment to extract instanceName if it is available. + /// Initializes a new instance of the class. /// - /// - /// - /// - private static readonly Regex NamedPipeRegex = new("pipe\\\\MSSQL\\$(.*?)\\\\", RegexOptions.Compiled); - - private static readonly ConcurrentDictionary ConnectionDetailCache = new(StringComparer.OrdinalIgnoreCase); + public SqlClientTraceInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariablesVENDORED().Build()) // TODO: RENAME WAS NECESSARY TO FIX AMBIGUITY + { + } - /// - /// Gets or sets a value indicating whether or not the should add the names of commands as the tag. Default - /// value: . - /// - /// - /// SetDbStatementForStoredProcedure is only supported on .NET - /// and .NET Core runtimes. - /// - public bool SetDbStatementForStoredProcedure { get; set; } = true; + internal SqlClientTraceInstrumentationOptions(IConfiguration configuration) + { + var databaseSemanticConvention = GetSemanticConventionOptIn(configuration); + this.EmitOldAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.Old); + this.EmitNewAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.New); + } /// /// Gets or sets a value indicating whether or not the public bool SetDbStatementForText { get; set; } - /// - /// Gets or sets a value indicating whether or not the should parse the DataSource on a - /// SqlConnection into server name, instance name, and/or port - /// connection-level attribute tags. Default value: . - /// - /// - /// - /// EnableConnectionLevelAttributes is supported on all runtimes. - /// - /// - /// The default behavior is to set the SqlConnection DataSource as the tag. - /// If enabled, SqlConnection DataSource will be parsed and the server name will be sent as the - /// or tag, - /// the instance name will be sent as the tag, - /// and the port will be sent as the tag if it is not 1433 (the default port). - /// - /// - public bool EnableConnectionLevelAttributes { get; set; } - /// /// Gets or sets an action to enrich an with the /// raw SqlCommand object. @@ -169,134 +117,13 @@ internal class SqlClientTraceInstrumentationOptions /// public bool RecordException { get; set; } - internal static SqlConnectionDetails ParseDataSource(string dataSource) - { - Match match = DataSourceRegex.Match(dataSource); - - string? serverHostName = match.Groups[2].Value; - string? serverIpAddress = null; - - string? instanceName; - - var uriHostNameType = Uri.CheckHostName(serverHostName); - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - serverIpAddress = serverHostName; - serverHostName = null; - } - - string maybeProtocol = match.Groups[1].Value; - bool isNamedPipe = maybeProtocol.Length > 0 && - maybeProtocol.StartsWith("np", StringComparison.OrdinalIgnoreCase); - - if (isNamedPipe) - { - string pipeName = match.Groups[3].Value; - if (pipeName.Length > 0) - { - var namedInstancePipeMatch = NamedPipeRegex.Match(pipeName); - if (namedInstancePipeMatch.Success) - { - instanceName = namedInstancePipeMatch.Groups[1].Value; - return new SqlConnectionDetails - { - ServerHostName = serverHostName, - ServerIpAddress = serverIpAddress, - InstanceName = instanceName, - Port = null, - }; - } - } - - return new SqlConnectionDetails - { - ServerHostName = serverHostName, - ServerIpAddress = serverIpAddress, - InstanceName = null, - Port = null, - }; - } - - string? port; - if (match.Groups[4].Length > 0) - { - instanceName = match.Groups[3].Value; - port = match.Groups[4].Value; - if (port == "1433") - { - port = null; - } - } - else if (int.TryParse(match.Groups[3].Value, out int parsedPort)) - { - port = parsedPort == 1433 ? null : match.Groups[3].Value; - instanceName = null; - } - else - { - instanceName = match.Groups[3].Value; - - if (string.IsNullOrEmpty(instanceName)) - { - instanceName = null; - } - - port = null; - } - - return new SqlConnectionDetails - { - ServerHostName = serverHostName, - ServerIpAddress = serverIpAddress, - InstanceName = instanceName, - Port = port, - }; - } - - internal void AddConnectionLevelDetailsToActivity(string dataSource, Activity sqlActivity) - { - if (!this.EnableConnectionLevelAttributes) - { - sqlActivity.SetTag(SemanticConventions.AttributePeerService, dataSource); - } - else - { - if (!ConnectionDetailCache.TryGetValue(dataSource, out SqlConnectionDetails? connectionDetails)) - { - connectionDetails = ParseDataSource(dataSource); - ConnectionDetailCache.TryAdd(dataSource, connectionDetails); - } - - if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) - { - sqlActivity.SetTag(SemanticConventions.AttributeDbMsSqlInstanceName, connectionDetails.InstanceName); - } - - if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) - { - sqlActivity.SetTag(SemanticConventions.AttributeServerAddress, connectionDetails.ServerHostName); - } - else - { - sqlActivity.SetTag(SemanticConventions.AttributeServerSocketAddress, connectionDetails.ServerIpAddress); - } - - if (!string.IsNullOrEmpty(connectionDetails.Port)) - { - // TODO: Should we continue to emit this if the default port (1433) is being used? - sqlActivity.SetTag(SemanticConventions.AttributeServerPort, connectionDetails.Port); - } - } - } - - internal sealed class SqlConnectionDetails - { - public string? ServerHostName { get; set; } - - public string? ServerIpAddress { get; set; } - - public string? InstanceName { get; set; } + /// + /// Gets or sets a value indicating whether the old database attributes should be emitted. + /// + internal bool EmitOldAttributes { get; set; } - public string? Port { get; set; } - } + /// + /// Gets or sets a value indicating whether the new database attributes should be emitted. + /// + internal bool EmitNewAttributes { get; set; } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs index c9a5b7b074a6..40b4833e3f14 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.DependencyInjection; @@ -22,7 +22,7 @@ internal static class TracerProviderBuilderExtensions /// /// being configured. /// The instance of to chain the calls. -#if NET6_0_OR_GREATER +#if NET [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] #endif public static TracerProviderBuilder AddSqlClientInstrumentation(this TracerProviderBuilder builder) @@ -34,7 +34,7 @@ public static TracerProviderBuilder AddSqlClientInstrumentation(this TracerProvi /// being configured. /// Callback action for configuring . /// The instance of to chain the calls. -#if NET6_0_OR_GREATER +#if NET [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] #endif public static TracerProviderBuilder AddSqlClientInstrumentation( @@ -49,10 +49,9 @@ public static TracerProviderBuilder AddSqlClientInstrumentation( /// Name which is used when retrieving options. /// Callback action for configuring . /// The instance of to chain the calls. -#if NET6_0_OR_GREATER +#if NET [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] #endif - public static TracerProviderBuilder AddSqlClientInstrumentation( this TracerProviderBuilder builder, string? name, @@ -70,8 +69,8 @@ public static TracerProviderBuilder AddSqlClientInstrumentation( builder.AddInstrumentation(sp => { var sqlOptions = sp.GetRequiredService>().Get(name); - - return new SqlClientInstrumentation(sqlOptions); + SqlClientInstrumentation.TracingOptions = sqlOptions; + return SqlClientInstrumentation.AddTracingHandle(); }); builder.AddSource(SqlActivitySourceHelper.ActivitySourceName); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs index 71b0c03a5c07..6163fed89841 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs @@ -22,7 +22,7 @@ internal sealed class AppServiceResourceDetector : IResourceDetector /// public Resource Detect() { - List> attributeList = new(); + List> attributeList = []; try { @@ -61,21 +61,18 @@ public Resource Detect() private static string? GetAzureResourceURI(string websiteSiteName) { - string? websiteResourceGroup = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceResourceGroupEnvVar); - string websiteOwnerName = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceOwnerNameEnvVar) ?? string.Empty; + var websiteResourceGroup = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceResourceGroupEnvVar); + var websiteOwnerName = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceOwnerNameEnvVar) ?? string.Empty; #if NET - int idx = websiteOwnerName.IndexOf('+', StringComparison.Ordinal); + var idx = websiteOwnerName.IndexOf('+', StringComparison.Ordinal); #else - int idx = websiteOwnerName.IndexOf("+", StringComparison.Ordinal); + var idx = websiteOwnerName.IndexOf("+", StringComparison.Ordinal); #endif - string subscriptionId = idx > 0 ? websiteOwnerName.Substring(0, idx) : websiteOwnerName; + var subscriptionId = idx > 0 ? websiteOwnerName.Substring(0, idx) : websiteOwnerName; - if (string.IsNullOrEmpty(websiteResourceGroup) || string.IsNullOrEmpty(subscriptionId)) - { - return null; - } - - return $"/subscriptions/{subscriptionId}/resourceGroups/{websiteResourceGroup}/providers/Microsoft.Web/sites/{websiteSiteName}"; + return string.IsNullOrEmpty(websiteResourceGroup) || string.IsNullOrEmpty(subscriptionId) + ? null + : $"/subscriptions/{subscriptionId}/resourceGroups/{websiteResourceGroup}/providers/Microsoft.Web/sites/{websiteSiteName}"; } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureContainerAppsResourceDetector.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureContainerAppsResourceDetector.cs index 58c11aa2bd19..aa04c2b35cc7 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureContainerAppsResourceDetector.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureContainerAppsResourceDetector.cs @@ -25,7 +25,7 @@ internal sealed class AzureContainerAppsResourceDetector : IResourceDetector /// public Resource Detect() { - List> attributeList = new List>(); + List> attributeList = []; try { var containerAppName = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AzureContainerAppsNameEnvVar); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVMResourceDetector.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVMResourceDetector.cs index 5553488629cb..3feeee3a9956 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVMResourceDetector.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVMResourceDetector.cs @@ -10,8 +10,8 @@ namespace OpenTelemetry.Resources.Azure; /// internal sealed class AzureVMResourceDetector : IResourceDetector { - internal static readonly IReadOnlyCollection ExpectedAzureAmsFields = new string[] - { + internal static readonly IReadOnlyCollection ExpectedAzureAmsFields = + [ ResourceAttributeConstants.AzureVmScaleSetName, ResourceAttributeConstants.AzureVmSku, ResourceSemanticConventions.AttributeCloudPlatform, @@ -23,8 +23,8 @@ internal sealed class AzureVMResourceDetector : IResourceDetector ResourceSemanticConventions.AttributeHostType, ResourceSemanticConventions.AttributeOsType, ResourceSemanticConventions.AttributeOsVersion, - ResourceSemanticConventions.AttributeServiceInstance, - }; + ResourceSemanticConventions.AttributeServiceInstance + ]; private static Resource? vmResource; diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVmMetadataResponse.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVmMetadataResponse.cs index 81418c8920e2..1282298feda9 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVmMetadataResponse.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Resources.Azure/AzureVmMetadataResponse.cs @@ -80,6 +80,8 @@ internal string GetValueForField(string fieldName) case ResourceAttributeConstants.AzureVmSku: amsValue = this.Sku; break; + default: + break; } amsValue ??= string.Empty; diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/AssemblyVersionExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/AssemblyVersionExtensions.cs index 4c71ae35988e..953de906cc8e 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/AssemblyVersionExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/AssemblyVersionExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Reflection; diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DatabaseSemanticConventionHelper.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DatabaseSemanticConventionHelper.cs new file mode 100644 index 000000000000..1225e43069d0 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DatabaseSemanticConventionHelper.cs @@ -0,0 +1,81 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace OpenTelemetry.Internal; + +/// +/// Helper class for Database Semantic Conventions. +/// +/// +/// Due to a breaking change in the semantic convention, affected instrumentation libraries +/// must inspect an environment variable to determine which attributes to emit. +/// This is expected to be removed when the instrumentation libraries reach Stable. +/// . +/// +internal static class DatabaseSemanticConventionHelper +{ + internal const string SemanticConventionOptInKeyName = "OTEL_SEMCONV_STABILITY_OPT_IN"; + internal static readonly char[] Separator = [',', ' ']; + + [Flags] + internal enum DatabaseSemanticConvention + { + /// + /// Instructs an instrumentation library to emit the old experimental Database attributes. + /// + Old = 0x1, + + /// + /// Instructs an instrumentation library to emit the new, v1.28.0 Database attributes. + /// + New = 0x2, + + /// + /// Instructs an instrumentation library to emit both the old and new attributes. + /// + Dupe = Old | New, + } + + public static DatabaseSemanticConvention GetSemanticConventionOptIn(IConfiguration configuration) + { + if (TryGetConfiguredValues(configuration, out var values)) + { + if (values.Contains("database/dup")) + { + return DatabaseSemanticConvention.Dupe; + } + else if (values.Contains("database")) + { + return DatabaseSemanticConvention.New; + } + } + + return DatabaseSemanticConvention.Old; + } + + private static bool TryGetConfiguredValues(IConfiguration configuration, [NotNullWhen(true)] out HashSet? values) + { + try + { + var stringValue = configuration[SemanticConventionOptInKeyName]; + + if (string.IsNullOrWhiteSpace(stringValue)) + { + values = null; + return false; + } + + var stringValues = stringValue!.Split(separator: Separator, options: StringSplitOptions.RemoveEmptyEntries); + values = new HashSet(stringValues, StringComparer.OrdinalIgnoreCase); + return true; + } + catch + { + values = null; + return false; + } + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceListener.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceListener.cs index 3ffad9bf33a7..9b0b07b3c813 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceListener.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceListener.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using OpenTelemetry.Internal; diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceSubscriber.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceSubscriber.cs index e01d14c3adb1..38898695c805 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceSubscriber.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/DiagnosticSourceSubscriber.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using OpenTelemetry.Internal; @@ -34,7 +32,7 @@ public DiagnosticSourceSubscriber( { Guard.ThrowIfNull(handlerFactory); - this.listenerSubscriptions = new List(); + this.listenerSubscriptions = []; this.handlerFactory = handlerFactory; this.diagnosticSourceFilter = diagnosticSourceFilter; this.isEnabledFilter = isEnabledFilter; @@ -43,10 +41,7 @@ public DiagnosticSourceSubscriber( public void Subscribe() { - if (this.allSourcesSubscription == null) - { - this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); - } + this.allSourcesSubscription ??= DiagnosticListener.AllListeners.Subscribe(this); } public void OnNext(DiagnosticListener value) @@ -91,15 +86,22 @@ private void Dispose(bool disposing) lock (this.listenerSubscriptions) { - foreach (var listenerSubscription in this.listenerSubscriptions) + if (disposing) { - listenerSubscription?.Dispose(); + foreach (var listenerSubscription in this.listenerSubscriptions) + { + listenerSubscription?.Dispose(); + } } this.listenerSubscriptions.Clear(); } - this.allSourcesSubscription?.Dispose(); + if (disposing) + { + this.allSourcesSubscription?.Dispose(); + } + this.allSourcesSubscription = null; } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesConfigurationProvider.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesConfigurationProvider.cs new file mode 100644 index 000000000000..8d58d6418020 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesConfigurationProvider.cs @@ -0,0 +1,85 @@ +// (Turns off StyleCop analysis in this file.) +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Configuration.EnvironmentVariables +{ + /// + /// An environment variable based . + /// + internal sealed class EnvironmentVariablesConfigurationProvider : ConfigurationProvider + { + private readonly string _prefix; + private readonly string _normalizedPrefix; + + /// + /// Initializes a new instance. + /// + public EnvironmentVariablesConfigurationProvider() + { + _prefix = string.Empty; + _normalizedPrefix = string.Empty; + } + + /// + /// Initializes a new instance with the specified prefix. + /// + /// A prefix used to filter the environment variables. + public EnvironmentVariablesConfigurationProvider(string? prefix) + { + _prefix = prefix ?? string.Empty; + _normalizedPrefix = Normalize(_prefix); + } + + /// + /// Loads the environment variables. + /// + public override void Load() => + Load(Environment.GetEnvironmentVariables()); + + /// + /// Generates a string representing this provider name and relevant details. + /// + /// The configuration name. + public override string ToString() + => $"{GetType().Name} Prefix: '{_prefix}'"; + + internal void Load(IDictionary envVariables) + { + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + + IDictionaryEnumerator e = envVariables.GetEnumerator(); + try + { + while (e.MoveNext()) + { + string key = (string)e.Entry.Key; + string? value = (string?)e.Entry.Value; + AddIfNormalizedKeyMatchesPrefix(data, Normalize(key), value); + } + } + finally + { + (e as IDisposable)?.Dispose(); + } + + Data = data; + } + + private void AddIfNormalizedKeyMatchesPrefix(Dictionary data, string normalizedKey, string? value) + { + if (normalizedKey.StartsWith(_normalizedPrefix, StringComparison.OrdinalIgnoreCase)) + { + data[normalizedKey.Substring(_normalizedPrefix.Length)] = value; + } + } + + private static string Normalize(string key) => key.Replace("__", ConfigurationPath.KeyDelimiter); + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesConfigurationSource.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesConfigurationSource.cs new file mode 100644 index 000000000000..86409bb8439e --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesConfigurationSource.cs @@ -0,0 +1,33 @@ +// (Turns off StyleCop analysis in this file.) +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETSTANDARD2_0 // HACK; FIX FOR AMBIGUITY + +#nullable enable + +namespace Microsoft.Extensions.Configuration.EnvironmentVariables +{ + /// + /// Represents environment variables as an . + /// + internal sealed class EnvironmentVariablesConfigurationSource : IConfigurationSource + { + /// + /// A prefix used to filter environment variables. + /// + public string? Prefix { get; set; } + + /// + /// Builds the for this source. + /// + /// The . + /// A + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new EnvironmentVariablesConfigurationProvider(Prefix); + } + } +} + +#endif diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesExtensions.cs new file mode 100644 index 000000000000..c4213f068be4 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariablesExtensions.cs @@ -0,0 +1,52 @@ +// (Turns off StyleCop analysis in this file.) +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using Microsoft.Extensions.Configuration.EnvironmentVariables; + +namespace Microsoft.Extensions.Configuration +{ + /// + /// Extension methods for registering with . + /// + internal static class EnvironmentVariablesExtensions + { + /// + /// Adds an that reads configuration values from environment variables. + /// + /// The to add to. + /// The . + public static IConfigurationBuilder AddEnvironmentVariablesVENDORED(this IConfigurationBuilder configurationBuilder) + { + configurationBuilder.Add(new EnvironmentVariablesConfigurationSource()); + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from environment variables + /// with a specified prefix. + /// + /// The to add to. + /// The prefix that environment variable names must start with. The prefix will be removed from the environment variable names. + /// The . + public static IConfigurationBuilder AddEnvironmentVariablesVENDORED( + this IConfigurationBuilder configurationBuilder, + string? prefix) + { + configurationBuilder.Add(new EnvironmentVariablesConfigurationSource { Prefix = prefix }); + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from environment variables. + /// + /// The to add to. + /// Configures the source. + /// The . + public static IConfigurationBuilder AddEnvironmentVariablesVENDORED(this IConfigurationBuilder builder, Action? configureSource) + => builder.Add(configureSource); + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ExceptionExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ExceptionExtensions.cs index 9070b59c2062..5071a8feebb8 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ExceptionExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ExceptionExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Globalization; namespace OpenTelemetry.Internal; diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/Guard.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/Guard.cs index 321099954789..d8b88d6a07f2 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/Guard.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/Guard.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - // Note: For some targets this file will contain more than one type/namespace. #pragma warning disable IDE0161 // Convert to file-scoped namespace @@ -59,7 +57,7 @@ internal static class Guard /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] object? value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfNull([NotNull] object? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { if (value is null) { @@ -74,7 +72,7 @@ public static void ThrowIfNull([NotNull] object? value, [CallerArgumentExpressio /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNullOrEmpty([NotNull] string? value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfNullOrEmpty([NotNull] string? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. { if (string.IsNullOrEmpty(value)) @@ -91,7 +89,7 @@ public static void ThrowIfNullOrEmpty([NotNull] string? value, [CallerArgumentEx /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNullOrWhitespace([NotNull] string? value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfNullOrWhitespace([NotNull] string? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. { if (string.IsNullOrWhiteSpace(value)) @@ -109,7 +107,7 @@ public static void ThrowIfNullOrWhitespace([NotNull] string? value, [CallerArgum /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfZero(int value, string message = "Must not be zero", [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfZero(int value, string message = "Must not be zero", [CallerArgumentExpression(nameof(value))] string? paramName = null) { if (value == 0) { @@ -124,7 +122,7 @@ public static void ThrowIfZero(int value, string message = "Must not be zero", [ /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfInvalidTimeout(int value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfInvalidTimeout(int value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { ThrowIfOutOfRange(value, paramName, min: Timeout.Infinite, message: $"Must be non-negative or '{nameof(Timeout)}.{nameof(Timeout.Infinite)}'"); } @@ -141,7 +139,7 @@ public static void ThrowIfInvalidTimeout(int value, [CallerArgumentExpression("v /// An optional custom message to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfOutOfRange(int value, [CallerArgumentExpression("value")] string? paramName = null, int min = int.MinValue, int max = int.MaxValue, string? minName = null, string? maxName = null, string? message = null) + public static void ThrowIfOutOfRange(int value, [CallerArgumentExpression(nameof(value))] string? paramName = null, int min = int.MinValue, int max = int.MaxValue, string? minName = null, string? maxName = null, string? message = null) { Range(value, paramName, min, max, minName, maxName, message); } @@ -158,7 +156,7 @@ public static void ThrowIfOutOfRange(int value, [CallerArgumentExpression("value /// An optional custom message to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfOutOfRange(double value, [CallerArgumentExpression("value")] string? paramName = null, double min = double.MinValue, double max = double.MaxValue, string? minName = null, string? maxName = null, string? message = null) + public static void ThrowIfOutOfRange(double value, [CallerArgumentExpression(nameof(value))] string? paramName = null, double min = double.MinValue, double max = double.MaxValue, string? minName = null, string? maxName = null, string? message = null) { Range(value, paramName, min, max, minName, maxName, message); } @@ -172,14 +170,11 @@ public static void ThrowIfOutOfRange(double value, [CallerArgumentExpression("va /// The value casted to the specified type. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T ThrowIfNotOfType([NotNull] object? value, [CallerArgumentExpression("value")] string? paramName = null) + public static T ThrowIfNotOfType([NotNull] object? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { - if (value is not T result) - { - throw new InvalidCastException($"Cannot cast '{paramName}' from '{value?.GetType().ToString() ?? "null"}' to '{typeof(T)}'"); - } - - return result; + return value is not T result + ? throw new InvalidCastException($"Cannot cast '{paramName}' from '{value?.GetType().ToString() ?? "null"}' to '{typeof(T)}'") + : result; } [DebuggerHidden] diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ListenerHandler.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ListenerHandler.cs index a7c6f9049dd2..d711aab6b06a 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ListenerHandler.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/ListenerHandler.cs @@ -3,8 +3,6 @@ using System.Diagnostics; -#nullable enable - namespace OpenTelemetry.Instrumentation; /// diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/PropertyFetcher.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/PropertyFetcher.cs index 7dbcda9afacb..5fd69f01c41f 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/PropertyFetcher.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/PropertyFetcher.cs @@ -1,8 +1,11 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable disable +// NOTE: This version of PropertyFetcher is AOT-compatible. +// Usages of the non-AOT-compatible version can be moved over to this one when they need to support AOT/trimming. +// Copied from https://github.com/open-telemetry/opentelemetry-dotnet/blob/86a6ba0b7f7ed1f5e84e5a6610e640989cd3ae9f/src/Shared/DiagnosticSourceInstrumentation/PropertyFetcher.cs#L30 +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace OpenTelemetry.Instrumentation; @@ -11,12 +14,13 @@ namespace OpenTelemetry.Instrumentation; /// PropertyFetcher fetches a property from an object. /// /// The type of the property being fetched. -#pragma warning disable CA1812 internal sealed class PropertyFetcher -#pragma warning restore CA1812 { +#if NET + private const string TrimCompatibilityMessage = "PropertyFetcher is used to access properties on objects dynamically by design and cannot be made trim compatible."; +#endif private readonly string propertyName; - private PropertyFetch innerFetcher; + private PropertyFetch? innerFetcher; /// /// Initializes a new instance of the class. @@ -27,19 +31,23 @@ public PropertyFetcher(string propertyName) this.propertyName = propertyName; } + public int NumberOfInnerFetchers => this.innerFetcher == null + ? 0 + : 1 + this.innerFetcher.NumberOfInnerFetchers; + /// /// Fetch the property from the object. /// /// Object to be fetched. - /// Property fetched. - public T Fetch(object obj) + /// Fetched value. +#if NET + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + public T Fetch(object? obj) { - if (!this.TryFetch(obj, out T value)) - { - throw new ArgumentException("Supplied object was null or did not match the expected type.", nameof(obj)); - } - - return value; + return !this.TryFetch(obj, out var value) + ? throw new ArgumentException("Supplied object was null or did not match the expected type.", nameof(obj)) + : value; } /// @@ -47,70 +55,161 @@ public T Fetch(object obj) /// /// Object to be fetched. /// Fetched value. - /// if the property was fetched. - public bool TryFetch(object obj, out T value) + /// if the property was fetched. +#if NET + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + public bool TryFetch( + object? obj, + [NotNullWhen(true)] + out T? value) { - if (obj == null) + var localInnerFetcher = this.innerFetcher; + return localInnerFetcher is null ? TryFetchRare(obj, this.propertyName, ref this.innerFetcher, out value) : localInnerFetcher.TryFetch(obj, out value); + } + +#if NET + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + private static bool TryFetchRare(object? obj, string propertyName, ref PropertyFetch? destination, out T? value) + { + if (obj is null) { value = default; return false; } - if (this.innerFetcher == null) - { - var type = obj.GetType().GetTypeInfo(); - var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, this.propertyName, StringComparison.OrdinalIgnoreCase)); - if (property == null) - { - property = type.GetProperty(this.propertyName); - } + var fetcher = PropertyFetch.Create(obj.GetType().GetTypeInfo(), propertyName); - this.innerFetcher = PropertyFetch.FetcherForProperty(property); + if (fetcher is null) + { + value = default; + return false; } - return this.innerFetcher.TryFetch(obj, out value); + destination = fetcher; + + return fetcher.TryFetch(obj, out value); } // see https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticSourceEventSource.cs - private class PropertyFetch +#if NET + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + private abstract class PropertyFetch { - /// - /// Create a property fetcher from a .NET Reflection PropertyInfo class that - /// represents a property of a particular type. - /// - public static PropertyFetch FetcherForProperty(PropertyInfo propertyInfo) + public abstract int NumberOfInnerFetchers { get; } + + public static PropertyFetch? Create(TypeInfo type, string propertyName) { - if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) + var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase)) ?? type.GetProperty(propertyName); + return CreateFetcherForProperty(property); + + static PropertyFetch? CreateFetcherForProperty(PropertyInfo? propertyInfo) { - // returns null on any fetch. - return new PropertyFetch(); - } + if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) + { + // returns null and wait for a valid payload to arrive. + return null; + } - var typedPropertyFetcher = typeof(TypedPropertyFetch<,>); - var instantiatedTypedPropertyFetcher = typedPropertyFetcher.MakeGenericType( - typeof(T), propertyInfo.DeclaringType, propertyInfo.PropertyType); - return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo); - } + var declaringType = propertyInfo.DeclaringType; + if (declaringType!.IsValueType) + { + throw new NotSupportedException( + $"Type: {declaringType.FullName} is a value type. PropertyFetcher can only operate on reference payload types."); + } - public virtual bool TryFetch(object obj, out T value) - { - value = default; - return false; + if (declaringType == typeof(object)) + { + // TODO: REMOVE this if branch when .NET 7 is out of support. + // This branch is never executed and is only needed for .NET 7 AOT-compiler at trimming stage; i.e., + // this is not needed in .NET 8, because the compiler is improved and call into MakeGenericMethod will be AOT-compatible. + // It is used to force the AOT compiler to create an instantiation of the method with a reference type. + // The code for that instantiation can then be reused at runtime to create instantiation over any other reference. + return CreateInstantiated(propertyInfo); + } + else + { + return DynamicInstantiationHelper(declaringType, propertyInfo); + } + + // Separated as a local function to be able to target the suppression to just this call. + // IL3050 was generated here because of the call to MakeGenericType, which is problematic in AOT if one of the type parameters is a value type; + // because the compiler might need to generate code specific to that type. + // If the type parameter is a reference type, there will be no problem; because the generated code can be shared among all reference type instantiations. +#if NET + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "The code guarantees that all the generic parameters are reference types.")] +#endif + static PropertyFetch? DynamicInstantiationHelper(Type declaringType, PropertyInfo propertyInfo) + { + return (PropertyFetch?)typeof(PropertyFetch) + .GetMethod(nameof(CreateInstantiated), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(declaringType) // This is validated in the earlier call chain to be a reference type. + .Invoke(null, [propertyInfo])!; + } + } } -#pragma warning disable CA1812 - private sealed class TypedPropertyFetch : PropertyFetch -#pragma warning restore CA1812 - where TDeclaredProperty : T + public abstract bool TryFetch( +#if NETSTANDARD2_1_0_OR_GREATER || NET + [NotNullWhen(true)] +#endif + object? obj, + out T? value); + + // Goal: make PropertyFetcher AOT-compatible. + // AOT compiler can't guarantee correctness when call into MakeGenericType or MakeGenericMethod + // if one of the generic parameters is a value type (reference types are OK.) + // For PropertyFetcher, the decision was made to only support reference type payloads, i.e.: + // the object from which to get the property value MUST be a reference type. + // Create generics with the declared object type as a generic parameter is OK, but we need the return type + // of the property to be a value type (on top of reference types.) + // Normally, we would have a helper class like `PropertyFetchInstantiated` that takes 2 generic parameters, + // the declared object type, and the type of the property value. + // But that would mean calling MakeGenericType, with value type parameters which AOT won't support. + // + // As a workaround, Generic instantiation was split into: + // 1. The object type comes from the PropertyFetcher generic parameter. + // Compiler supports it even if it is a value type; the type is known statically during compilation + // since PropertyFetcher is used with it. + // 2. Then, the declared object type is passed as a generic parameter to a generic method on PropertyFetcher (or nested type.) + // Therefore, calling into MakeGenericMethod will only require specifying one parameter - the declared object type. + // The declared object type is guaranteed to be a reference type (throw on value type.) Thus, MakeGenericMethod is AOT compatible. + private static PropertyFetchInstantiated CreateInstantiated(PropertyInfo propertyInfo) + where TDeclaredObject : class + => new PropertyFetchInstantiated(propertyInfo); + +#if NET + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + private sealed class PropertyFetchInstantiated : PropertyFetch + where TDeclaredObject : class { - private readonly Func propertyFetch; + private readonly string propertyName; + private readonly Func propertyFetch; + private PropertyFetch? innerFetcher; - public TypedPropertyFetch(PropertyInfo property) + public PropertyFetchInstantiated(PropertyInfo property) { - this.propertyFetch = (Func)property.GetMethod.CreateDelegate(typeof(Func)); + this.propertyName = property.Name; +#if NET + this.propertyFetch = property.GetMethod!.CreateDelegate>(); +#else + this.propertyFetch = (Func)property.GetMethod!.CreateDelegate(typeof(Func)); +#endif } - public override bool TryFetch(object obj, out T value) + public override int NumberOfInnerFetchers => this.innerFetcher == null + ? 0 + : 1 + this.innerFetcher.NumberOfInnerFetchers; + + public override bool TryFetch( +#if NETSTANDARD2_1_0_OR_GREATER || NET + [NotNullWhen(true)] +#endif + object? obj, + out T? value) { if (obj is TDeclaredObject o) { @@ -118,8 +217,8 @@ public override bool TryFetch(object obj, out T value) return true; } - value = default; - return false; + var innerFetcher = this.innerFetcher; + return innerFetcher is null ? TryFetchRare(obj, this.propertyName, ref this.innerFetcher, out value) : innerFetcher.TryFetch(obj, out value); } } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SemanticConventions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SemanticConventions.cs index 9f1c1ee234e8..8be40f76f7b0 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SemanticConventions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SemanticConventions.cs @@ -25,8 +25,6 @@ internal static class SemanticConventions public const string AttributeEnduserRole = "enduser.role"; public const string AttributeEnduserScope = "enduser.scope"; - public const string AttributePeerService = "peer.service"; - public const string AttributeHttpMethod = "http.method"; public const string AttributeHttpUrl = "http.url"; public const string AttributeHttpTarget = "http.target"; @@ -44,7 +42,6 @@ internal static class SemanticConventions public const string AttributeHttpResponseContentLength = "http.response_content_length"; public const string AttributeHttpResponseContentLengthUncompressed = "http.response_content_length_uncompressed"; - public const string AttributeDbSystem = "db.system"; public const string AttributeDbConnectionString = "db.connection_string"; public const string AttributeDbUser = "db.user"; public const string AttributeDbMsSqlInstanceName = "db.mssql.instance_name"; @@ -117,6 +114,11 @@ internal static class SemanticConventions public const string AttributeServerPort = "server.port"; // replaces: "net.host.port" (AttributeNetHostPort) public const string AttributeUserAgentOriginal = "user_agent.original"; // replaces: http.user_agent (AttributeHttpUserAgent) + // v1.23.0 Database spans + // https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/database/database-spans.md + public const string AttributeNetworkPeerAddress = "network.peer.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) + public const string AttributeNetworkPeerPort = "network.peer.port"; // replaces: "net.peer.port" (AttributeNetPeerPort) + // v1.24.0 Messaging spans // https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/messaging/messaging-spans.md public const string AttributeMessagingClientId = "messaging.client_id"; @@ -136,5 +138,16 @@ internal static class SemanticConventions public const string AttributeMessagingKafkaMessageKey = "messaging.kafka.message.key"; public const string AttributeMessagingKafkaMessageOffset = "messaging.kafka.message.offset"; + // New database conventions as of commit: + // https://github.com/open-telemetry/semantic-conventions/blob/25f74191d749645fdd5ec42ae661438cf2c1cf51/docs/database/database-spans.md#common-attributes + public const string AttributeDbSystem = "db.system"; + public const string AttributeDbCollectionName = "db.collection.name"; + public const string AttributeDbNamespace = "db.namespace"; + public const string AttributeDbOperationName = "db.operation.name"; + public const string AttributeDbResponseStatusCode = "db.response.status_code"; + public const string AttributeDbOperationBatchSize = "db.operation.batch.size"; + public const string AttributeDbQuerySummary = "db.query.summary"; + public const string AttributeDbQueryText = "db.query.text"; + #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SqlProcessor.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SqlProcessor.cs new file mode 100644 index 000000000000..c23a7c8a28a8 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/SqlProcessor.cs @@ -0,0 +1,264 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections; +using System.Text; + +namespace OpenTelemetry.Instrumentation; + +internal static class SqlProcessor +{ + private const int CacheCapacity = 1000; + private static readonly Hashtable Cache = []; + + public static string GetSanitizedSql(string? sql) + { + if (sql == null) + { + return string.Empty; + } + + if (Cache[sql] is not string sanitizedSql) + { + sanitizedSql = SanitizeSql(sql); + + if (Cache.Count == CacheCapacity) + { + return sanitizedSql; + } + + lock (Cache) + { + if ((Cache[sql] as string) == null) + { + if (Cache.Count < CacheCapacity) + { + Cache[sql] = sanitizedSql; + } + } + } + } + + return sanitizedSql!; + } + + private static string SanitizeSql(string sql) + { + var sb = new StringBuilder(capacity: sql.Length); + for (var i = 0; i < sql.Length; ++i) + { + if (SkipComment(sql, ref i)) + { + continue; + } + + if (SanitizeStringLiteral(sql, ref i) || + SanitizeHexLiteral(sql, ref i) || + SanitizeNumericLiteral(sql, ref i)) + { + sb.Append('?'); + continue; + } + + WriteToken(sql, ref i, sb); + } + + return sb.ToString(); + } + + private static bool SkipComment(string sql, ref int index) + { + var i = index; + var ch = sql[i]; + var length = sql.Length; + + // Scan past multi-line comment + if (ch == '/' && i + 1 < length && sql[i + 1] == '*') + { + for (i += 2; i < length; ++i) + { + ch = sql[i]; + if (ch == '*' && i + 1 < length && sql[i + 1] == '/') + { + i += 1; + break; + } + } + + index = i; + return true; + } + + // Scan past single-line comment + if (ch == '-' && i + 1 < length && sql[i + 1] == '-') + { + for (i += 2; i < length; ++i) + { + ch = sql[i]; + if (ch is '\r' or '\n') + { + i -= 1; + break; + } + } + + index = i; + return true; + } + + return false; + } + + private static bool SanitizeStringLiteral(string sql, ref int index) + { + var ch = sql[index]; + if (ch == '\'') + { + var i = index + 1; + var length = sql.Length; + for (; i < length; ++i) + { + ch = sql[i]; + if (ch == '\'' && i + 1 < length && sql[i + 1] == '\'') + { + ++i; + continue; + } + + if (ch == '\'') + { + break; + } + } + + index = i; + return true; + } + + return false; + } + + private static bool SanitizeHexLiteral(string sql, ref int index) + { + var i = index; + var ch = sql[i]; + var length = sql.Length; + + if (ch == '0' && i + 1 < length && (sql[i + 1] == 'x' || sql[i + 1] == 'X')) + { + for (i += 2; i < length; ++i) + { + ch = sql[i]; + if (char.IsDigit(ch) || + ch == 'A' || ch == 'a' || + ch == 'B' || ch == 'b' || + ch == 'C' || ch == 'c' || + ch == 'D' || ch == 'd' || + ch == 'E' || ch == 'e' || + ch == 'F' || ch == 'f') + { + continue; + } + + i -= 1; + break; + } + + index = i; + return true; + } + + return false; + } + + private static bool SanitizeNumericLiteral(string sql, ref int index) + { + var i = index; + var ch = sql[i]; + var length = sql.Length; + + // Scan past leading sign + if ((ch == '-' || ch == '+') && i + 1 < length && (char.IsDigit(sql[i + 1]) || sql[i + 1] == '.')) + { + i += 1; + ch = sql[i]; + } + + // Scan past leading decimal point + var periodMatched = false; + if (ch == '.' && i + 1 < length && char.IsDigit(sql[i + 1])) + { + periodMatched = true; + i += 1; + ch = sql[i]; + } + + if (char.IsDigit(ch)) + { + var exponentMatched = false; + for (i += 1; i < length; ++i) + { + ch = sql[i]; + if (char.IsDigit(ch)) + { + continue; + } + + if (!periodMatched && ch == '.') + { + periodMatched = true; + continue; + } + + if (!exponentMatched && (ch == 'e' || ch == 'E')) + { + // Scan past sign in exponent + if (i + 1 < length && (sql[i + 1] == '-' || sql[i + 1] == '+')) + { + i += 1; + } + + exponentMatched = true; + continue; + } + + i -= 1; + break; + } + + index = i; + return true; + } + + return false; + } + + private static void WriteToken(string sql, ref int index, StringBuilder sb) + { + var i = index; + var ch = sql[i]; + + if (char.IsLetter(ch) || ch == '_') + { + for (; i < sql.Length; i++) + { + ch = sql[i]; + if (char.IsLetter(ch) || ch == '_' || char.IsDigit(ch)) + { + sb.Append(ch); + continue; + } + + break; + } + + i -= 1; + } + else + { + sb.Append(ch); + } + + index = i; + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/HttpClientInstrumentationTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/HttpClientInstrumentationTests.cs index 3c98af3dce8b..eb26bb73d525 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/HttpClientInstrumentationTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/HttpClientInstrumentationTests.cs @@ -56,9 +56,6 @@ public async Task HttpRequestsAreCapturedCorrectly(string? queryString, int expe var activities = new List(); var serviceCollection = new ServiceCollection(); - // This shouldn't be needed but Http Instrumentation library was performing redaction without it. - serviceCollection.AddEnvironmentVariables(new Dictionary { { "OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION", "true" } }); - serviceCollection.AddOpenTelemetry() .UseAzureMonitor(x => x.ConnectionString = testConnectionString) .WithTracing(x => x.AddInMemoryExporter(activities)) @@ -87,6 +84,18 @@ public async Task HttpRequestsAreCapturedCorrectly(string? queryString, int expe string url = queryString is null ? path : path + queryString; + + // TODO: THIS NEEDS TO BE INVETIGATED. DISTRO SHOULD BE DISABLING REDACTION. + string expectedQueryString = queryString is null + ? string.Empty +#if NET9_0_OR_GREATER //Starting with .NET 9, HttpClient library performs redaction by default + : "?*"; +#else // For all older frameworks, the Instrumentation Library performs redaction by default + : "?key=Redacted"; +#endif + + string urlForValidation = path + expectedQueryString; + var httpclient = new HttpClient(); try @@ -111,9 +120,9 @@ public async Task HttpRequestsAreCapturedCorrectly(string? queryString, int expe VerifyTelemetryItem( isSuccessfulRequest: expectedStatusCode == 200, hasException: shouldThrowException, - expectedUrl: url, + expectedUrl: urlForValidation, operationName: $"GET {path}", - expectedData: baseAddress + path + (queryString ?? ""), + expectedData: baseAddress + path + expectedQueryString, expectedTarget: $"{host}:{port}", statusCode: expectedStatusCode.ToString(), telemetryItem: telemetryItem, diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/SqlClientInstrumentationTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/SqlClientInstrumentationTests.cs index b72ca1014627..5b13f3c84ce5 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/SqlClientInstrumentationTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/E2ETests/SqlClientInstrumentationTests.cs @@ -174,7 +174,7 @@ public void SqlClientCallsAreCollectedSuccessfully( serviceCollection.Configure(options => { options.SetDbStatementForText = captureTextCommandContent; - options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; + //options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; // TODO: THIS WAS REMOVED: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2284 if (shouldEnrich) { options.Enrich = ActivityEnrichment; diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/ActivityEventTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/ActivityEventTests.cs index e54461d43040..df826c4e3336 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/ActivityEventTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/ActivityEventTests.cs @@ -47,7 +47,7 @@ public void VerifyActivityWithExceptions() catch (System.Exception ex) { activity.SetStatus(ActivityStatusCode.Error); - activity.RecordException(ex, new TagList + activity.AddException(ex, new TagList { {"customKey1", "customValue1"}, {"customKey2", "customValue2"}, diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/SqlClientDependencyTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/SqlClientDependencyTests.cs index fa6a782ff2bb..45baa44e09a3 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/SqlClientDependencyTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Tests/LiveMetrics/DocumentTests/SqlClientDependencyTests.cs @@ -99,7 +99,7 @@ public void VerifySqlClientDependency( .AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; - options.SetDbStatementForStoredProcedure = true; + //options.SetDbStatementForStoredProcedure = true; // TODO: THIS WAS REMOVED: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2284 }) .AddInMemoryExporter(exportedActivities) .Build()) @@ -170,7 +170,7 @@ public void VerifySqlClientDependencyWithException( .AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; - options.SetDbStatementForStoredProcedure = true; + //options.SetDbStatementForStoredProcedure = true; // TODO: THIS WAS REMOVED: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2284 options.RecordException = recordException; }) .AddInMemoryExporter(exportedActivities) diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.LiveMetrics.Demo/Program.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.LiveMetrics.Demo/Program.cs index e46445990370..2e7e60357205 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.LiveMetrics.Demo/Program.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.LiveMetrics.Demo/Program.cs @@ -101,7 +101,7 @@ private static async Task GenerateTelemetry() catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error); - activity?.RecordException(ex, new TagList { { "customKey1", "customValue1" } }); + activity?.AddException(ex, new TagList { { "customKey1", "customValue1" } }); } } else @@ -130,7 +130,7 @@ private static async Task GenerateTelemetry() catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error); - activity?.RecordException(ex, new TagList { { "customKey1", "customValue1" } }); + activity?.AddException(ex, new TagList { { "customKey1", "customValue1" } }); } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md index 875f2eb78dce..933e45bf2034 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md @@ -16,6 +16,11 @@ ### Other Changes +* Update OpenTelemetry dependencies + ([#48574](https://github.com/Azure/azure-sdk-for-net/pull/48574)) + - OpenTelemetry 1.11.2 + - OpenTelemetry.PersistentStorage.FileSystem 1.0.1 + ## 1.4.0-beta.2 (2024-10-11) ### Bugs Fixed diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindClient.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindClient.cs index 210f2c053fe6..6968153c36f3 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindClient.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindClient.cs @@ -52,7 +52,7 @@ public ActivityKindClient(PerfOptions options) : base(options) parentContext: new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded), startTime: DateTime.UtcNow); - activity?.SetStatus(Status.Ok); + activity?.SetStatus(ActivityStatusCode.Ok); activity?.SetTag(SemanticConventions.AttributeHttpScheme, "https"); activity?.SetTag(SemanticConventions.AttributeHttpMethod, "POST"); activity?.SetTag(SemanticConventions.AttributeHttpTarget, "api/123"); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindServer.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindServer.cs index f4ef1769b06e..cba2f2c55e78 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindServer.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/perf/Scenarios/ActivityKindServer.cs @@ -52,7 +52,7 @@ public ActivityKindServer(PerfOptions options) : base(options) parentContext: new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded), startTime: DateTime.UtcNow); - activity?.SetStatus(Status.Ok); + activity?.SetStatus(ActivityStatusCode.Ok); activity?.SetTag(SemanticConventions.AttributeHttpMethod, "Get"); activity?.SetTag(SemanticConventions.AttributeHttpScheme, "https"); activity?.SetTag(SemanticConventions.AttributeHttpTarget, "api/123"); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/TraceHelper.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/TraceHelper.cs index 6ec78d04a0f1..d586d547dca2 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/TraceHelper.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/TraceHelper.cs @@ -316,7 +316,7 @@ private static void AddTelemetryFromActivityEvents(Activity activity, TelemetryI } } - if (exceptionMessage == null || exceptionType == null) + if (string.IsNullOrEmpty(exceptionMessage) || exceptionType == null) { return null; } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Demo/Traces/TraceDemo.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Demo/Traces/TraceDemo.cs index 61136d052043..4753ac0fb7a1 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Demo/Traces/TraceDemo.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Demo/Traces/TraceDemo.cs @@ -74,7 +74,7 @@ public void GenerateTraces() catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error); - activity?.RecordException(ex); + activity?.AddException(ex); } } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/TracesTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/TracesTests.cs index 98c934a505d3..8753b235aa0d 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/TracesTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/TracesTests.cs @@ -171,7 +171,7 @@ public void VerifyExceptionWithinActivity() catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error); - activity?.RecordException(ex, new TagList + activity?.AddException(ex, new TagList { { "someKey", "someValue" }, }); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataNewTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataNewTests.cs index c0e6d570948d..ebd1cb5d0619 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataNewTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataNewTests.cs @@ -78,7 +78,7 @@ public void ValidateHttpRemoteDependencyData() activity.Stop(); var httpUrl = "https://www.foo.bar/search"; - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "GET"); activity.SetTag(SemanticConventions.AttributeUrlFull, httpUrl); activity.SetTag(SemanticConventions.AttributeServerAddress, "www.foo.bar"); @@ -94,7 +94,7 @@ public void ValidateHttpRemoteDependencyData() Assert.Equal("www.foo.bar", remoteDependencyData.Target); Assert.Equal("200", remoteDependencyData.ResultCode); Assert.Equal(activity.Duration.ToString("c", CultureInfo.InvariantCulture), remoteDependencyData.Duration); - Assert.Equal(activity.GetStatus() != Status.Error, remoteDependencyData.Success); + Assert.Equal(activity.Status != ActivityStatusCode.Error, remoteDependencyData.Success); Assert.True(remoteDependencyData.Properties.Count == 0); Assert.True(remoteDependencyData.Measurements.Count == 0); } @@ -135,7 +135,7 @@ public void ValidateMessagingRemoteDependencyData() Assert.NotNull(activity); activity.Stop(); - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeMessagingSystem, "servicebus"); activity.SetTag(SemanticConventions.AttributeServerAddress, "my.servicebus.windows.net"); activity.SetTag(SemanticConventions.AttributeMessagingDestinationName, "queueName"); @@ -150,7 +150,7 @@ public void ValidateMessagingRemoteDependencyData() Assert.Null(remoteDependencyData.ResultCode); Assert.Equal(activity.Duration.ToString("c", CultureInfo.InvariantCulture), remoteDependencyData.Duration); Assert.Equal("my.servicebus.windows.net/queueName", remoteDependencyData.Target); - Assert.Equal(activity.GetStatus() != Status.Error, remoteDependencyData.Success); + Assert.Equal(activity.Status != ActivityStatusCode.Error, remoteDependencyData.Success); Assert.True(remoteDependencyData.Properties.Count == 0); Assert.True(remoteDependencyData.Measurements.Count == 0); } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataTests.cs index f2108aa0af22..a9ff8b9873a0 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RemoteDependencyDataTests.cs @@ -114,7 +114,7 @@ public void ValidateHttpRemoteDependencyData() activity.Stop(); var httpUrl = "https://www.foo.bar/search"; - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeHttpMethod, "GET"); activity.SetTag(SemanticConventions.AttributeHttpUrl, httpUrl); // only adding test via http.url. all possible combinations are covered in AzMonListExtensionsTests. activity.SetTag(SemanticConventions.AttributeHttpHost, "www.foo.bar"); @@ -130,7 +130,7 @@ public void ValidateHttpRemoteDependencyData() Assert.Equal("www.foo.bar", remoteDependencyData.Target); Assert.Equal("200", remoteDependencyData.ResultCode); Assert.Equal(activity.Duration.ToString("c", CultureInfo.InvariantCulture), remoteDependencyData.Duration); - Assert.Equal(activity.GetStatus() != Status.Error, remoteDependencyData.Success); + Assert.Equal(activity.Status != ActivityStatusCode.Error, remoteDependencyData.Success); Assert.True(remoteDependencyData.Properties.Count == 0); Assert.True(remoteDependencyData.Measurements.Count == 0); } @@ -147,7 +147,7 @@ public void ValidateDbRemoteDependencyData() Assert.NotNull(activity); activity.Stop(); - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeDbName, "mysqlserver"); activity.SetTag(SemanticConventions.AttributeDbSystem, "mssql"); activity.SetTag(SemanticConventions.AttributePeerService, "localhost"); // only adding test via peer.service. all possible combinations are covered in AzMonListExtensionsTests. @@ -163,7 +163,7 @@ public void ValidateDbRemoteDependencyData() Assert.Equal("localhost | mysqlserver", remoteDependencyData.Target); Assert.Null(remoteDependencyData.ResultCode); Assert.Equal(activity.Duration.ToString("c", CultureInfo.InvariantCulture), remoteDependencyData.Duration); - Assert.Equal(activity.GetStatus() != Status.Error, remoteDependencyData.Success); + Assert.Equal(activity.Status != ActivityStatusCode.Error, remoteDependencyData.Success); Assert.True(remoteDependencyData.Properties.Count == 1); Assert.True(remoteDependencyData.Properties.Contains(new KeyValuePair(SemanticConventions.AttributeDbName, "mysqlserver" ))); Assert.True(remoteDependencyData.Measurements.Count == 0); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataNewTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataNewTests.cs index f2ef70838e35..c56ef5a8b587 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataNewTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataNewTests.cs @@ -32,7 +32,7 @@ public void ValidateHttpRequestData() activity.Stop(); var httpUrl = "https://www.foo.bar/search"; - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, "GET"); activity.SetTag(SemanticConventions.AttributeHttpRoute, "/search"); activity.SetTag(SemanticConventions.AttributeUrlScheme, "https"); @@ -51,7 +51,7 @@ public void ValidateHttpRequestData() Assert.Equal(httpUrl, requestData.Url); Assert.Equal("0", requestData.ResponseCode); Assert.Equal(activity.Duration.ToString("c", CultureInfo.InvariantCulture), requestData.Duration); - Assert.False(requestData.Success); + Assert.True(requestData.Success); Assert.Null(requestData.Source); Assert.True(requestData.Properties.Count == 1); Assert.Equal("bar", requestData.Properties["foo"]); @@ -236,7 +236,7 @@ public void ValidateMessagingRequestData() Assert.NotNull(activity); activity.Stop(); - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeMessagingSystem, "servicebus"); activity.SetTag(SemanticConventions.AttributeServerAddress, "my.servicebus.windows.net"); activity.SetTag(SemanticConventions.AttributeMessagingDestinationName, "queueName"); diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataTests.cs index f8711b05b99e..51d61aaa7368 100755 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/RequestDataTests.cs @@ -46,7 +46,7 @@ public void ValidateHttpRequestData() activity.Stop(); var httpUrl = "https://www.foo.bar/search"; - activity.SetStatus(Status.Ok); + activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SemanticConventions.AttributeHttpMethod, "GET"); activity.SetTag(SemanticConventions.AttributeHttpRoute, "/search"); activity.SetTag(SemanticConventions.AttributeHttpUrl, httpUrl); // only adding test via http.url. all possible combinations are covered in AzMonListExtensionsTests. diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/TraceHelperTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/TraceHelperTests.cs index b327957579c1..d30bd55a7d8e 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/TraceHelperTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/TraceHelperTests.cs @@ -237,7 +237,7 @@ public void ActivityWithExceptionEventCreatesExceptionTelemetry() ActivityKind.Server); Assert.NotNull(activity); - activity.RecordException(new Exception(exceptionMessage)); + activity.AddException(new Exception(exceptionMessage)); Activity[] activityList = new Activity[1]; activityList[0] = activity; @@ -307,7 +307,7 @@ public void ActivityWithExceptionEventDoesNotCreateExceptionTelemetryWhenExcepti // Checking with empty string here as OTel // adds the exception only if it non-null and non-empty. // https://github.com/open-telemetry/opentelemetry-dotnet/blob/872a52f5291804c7af19e90307b5cc097b2da709/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs#L102-L104 - activity.RecordException(new Exception("")); + activity.AddException(new Exception("")); Activity[] activityList = new Activity[1]; activityList[0] = activity;