diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs index f13de811e8b9f..11aa7d65bd1a1 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/AzureMonitorAspNetCoreEventSource.cs @@ -53,5 +53,14 @@ public void ConfigureFailed(Exception ex) [Event(1, Message = "Failed to configure AzureMonitorOptions using the connection string from environment variables due to an exception: {0}", Level = EventLevel.Error)] public void ConfigureFailed(string exceptionMessage) => WriteEvent(1, exceptionMessage); + + [Event(2, Message = "Package reference for {0} found. Backing off from default included instrumentation. Action Required: You must manually configure this instrumentation.", Level = EventLevel.Warning)] + public void FoundInstrumentationPackageReference(string packageName) => WriteEvent(2, packageName); + + [Event(3, Message = "No instrumentation package found with name: {0}.", Level = EventLevel.Verbose)] + public void NoInstrumentationPackageReference(string packageName) => WriteEvent(3, packageName); + + [Event(4, Message = "Vendor instrumentation added for: {0}.", Level = EventLevel.Verbose)] + public void VendorInstrumentationAdded(string packageName) => WriteEvent(4, packageName); } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs index f557a1c4c7a06..7864054779c7b 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/OpenTelemetryBuilderExtensions.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; using Azure.Monitor.OpenTelemetry.Exporter; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -23,6 +26,10 @@ namespace Azure.Monitor.OpenTelemetry.AspNetCore /// public static class OpenTelemetryBuilderExtensions { + private const string AspNetCoreInstrumentationPackageName = "OpenTelemetry.Instrumentation.AspNetCore"; + private const string HttpClientInstrumentationPackageName = "OpenTelemetry.Instrumentation.Http"; + private const string SqlClientInstrumentationPackageName = "OpenTelemetry.Instrumentation.SqlClient"; + /// /// Configures Azure Monitor for logging, distributed tracing, and metrics. /// @@ -102,23 +109,7 @@ public static OpenTelemetryBuilder UseAzureMonitor(this OpenTelemetryBuilder bui builder.WithTracing(b => b .AddSource("Azure.*") - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation(o => o.FilterHttpRequestMessage = (_) => - { - // Azure SDKs create their own client span before calling the service using HttpClient - // In this case, we would see two spans corresponding to the same operation - // 1) created by Azure SDK 2) created by HttpClient - // To prevent this duplication we are filtering the span from HttpClient - // as span from Azure SDK contains all relevant information needed. - var parentActivity = Activity.Current?.Parent; - if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http")) - { - return false; - } - - return true; - }) - .AddSqlClientInstrumentation() + .AddVendorInstrumentationIfPackageNotReferenced() .AddAzureMonitorTraceExporter()); builder.WithMetrics(b => b @@ -158,5 +149,55 @@ public static OpenTelemetryBuilder UseAzureMonitor(this OpenTelemetryBuilder bui return builder; } + + private static TracerProviderBuilder AddVendorInstrumentationIfPackageNotReferenced(this TracerProviderBuilder tracerProviderBuilder) + { + var vendorInstrumentationActions = new Dictionary + { + { AspNetCoreInstrumentationPackageName, () => tracerProviderBuilder.AddAspNetCoreInstrumentation() }, + { SqlClientInstrumentationPackageName, () => tracerProviderBuilder.AddSqlClientInstrumentation() }, + { + HttpClientInstrumentationPackageName, + () => tracerProviderBuilder.AddHttpClientInstrumentation(o => o.FilterHttpRequestMessage = (_) => + { + // Azure SDKs create their own client span before calling the service using HttpClient + // In this case, we would see two spans corresponding to the same operation + // 1) created by Azure SDK 2) created by HttpClient + // To prevent this duplication we are filtering the span from HttpClient + // as span from Azure SDK contains all relevant information needed. + var parentActivity = Activity.Current?.Parent; + if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http")) + { + return false; + } + + return true; + }) + }, + }; + + foreach (var packageActionPair in vendorInstrumentationActions) + { + Assembly? instrumentationAssembly = null; + + try + { + instrumentationAssembly = Assembly.Load(packageActionPair.Key + ".dll"); + AzureMonitorAspNetCoreEventSource.Log.FoundInstrumentationPackageReference(packageActionPair.Key); + } + catch + { + AzureMonitorAspNetCoreEventSource.Log.NoInstrumentationPackageReference(packageActionPair.Key); + } + + if (instrumentationAssembly == null) + { + packageActionPair.Value.Invoke(); + AzureMonitorAspNetCoreEventSource.Log.VendorInstrumentationAdded(packageActionPair.Key); + } + } + + return tracerProviderBuilder; + } } } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.AspNetCore/GrpcTagHelper.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.AspNetCore/GrpcTagHelper.cs new file mode 100644 index 0000000000000..14f5d7b6164d0 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.AspNetCore/GrpcTagHelper.cs @@ -0,0 +1,89 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics; +using System.Text.RegularExpressions; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +internal static class GrpcTagHelper +{ + public const string RpcSystemGrpc = "grpc"; + + // The Grpc.Net.Client library adds its own tags to the activity. + // These tags are used to source the tags added by the OpenTelemetry instrumentation. + public const string GrpcMethodTagName = "grpc.method"; + public const string GrpcStatusCodeTagName = "grpc.status_code"; + + private static readonly Regex GrpcMethodRegex = new(@"^/?(?.*)/(?.*)$", RegexOptions.Compiled); + + public static string GetGrpcMethodFromActivity(Activity activity) + { + return activity.GetTagValue(GrpcMethodTagName) as string; + } + + public static bool TryGetGrpcStatusCodeFromActivity(Activity activity, out int statusCode) + { + statusCode = -1; + var grpcStatusCodeTag = activity.GetTagValue(GrpcStatusCodeTagName); + if (grpcStatusCodeTag == null) + { + return false; + } + + return int.TryParse(grpcStatusCodeTag as string, out statusCode); + } + + public static bool TryParseRpcServiceAndRpcMethod(string grpcMethod, out string rpcService, out string rpcMethod) + { + var match = GrpcMethodRegex.Match(grpcMethod); + if (match.Success) + { + rpcService = match.Groups["service"].Value; + rpcMethod = match.Groups["method"].Value; + return true; + } + else + { + rpcService = string.Empty; + rpcMethod = string.Empty; + return false; + } + } + + /// + /// Helper method that populates span properties from RPC status code according + /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#status. + /// + /// RPC status code. + /// Resolved span for the Grpc status code. + public static ActivityStatusCode ResolveSpanStatusForGrpcStatusCode(int statusCode) + { + var status = ActivityStatusCode.Error; + + if (typeof(StatusCanonicalCode).IsEnumDefined(statusCode)) + { + status = ((StatusCanonicalCode)statusCode) switch + { + StatusCanonicalCode.Ok => ActivityStatusCode.Unset, + _ => ActivityStatusCode.Error, + }; + } + + return status; + } +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.AspNetCore/StatusCanonicalCode.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.AspNetCore/StatusCanonicalCode.cs new file mode 100644 index 0000000000000..bd8be72a74a95 --- /dev/null +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/OpenTelemetry.Instrumentation.AspNetCore/StatusCanonicalCode.cs @@ -0,0 +1,148 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// Canonical result code of span execution. +/// +/// +/// This follows the standard GRPC codes. +/// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md. +/// +internal enum StatusCanonicalCode +{ + /// + /// The operation completed successfully. + /// + Ok = 0, + + /// + /// The operation was cancelled (typically by the caller). + /// + Cancelled = 1, + + /// + /// Unknown error. An example of where this error may be returned is if a Status value received + /// from another address space belongs to an error-space that is not known in this address space. + /// Also errors raised by APIs that do not return enough error information may be converted to + /// this error. + /// + Unknown = 2, + + /// + /// Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. + /// INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the + /// system (e.g., a malformed file name). + /// + InvalidArgument = 3, + + /// + /// Deadline expired before operation could complete. For operations that change the state of the + /// system, this error may be returned even if the operation has completed successfully. For + /// example, a successful response from a server could have been delayed long enough for the + /// deadline to expire. + /// + DeadlineExceeded = 4, + + /// + /// Some requested entity (e.g., file or directory) was not found. + /// + NotFound = 5, + + /// + /// Some entity that we attempted to create (e.g., file or directory) already exists. + /// + AlreadyExists = 6, + + /// + /// The caller does not have permission to execute the specified operation. PERMISSION_DENIED + /// must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED + /// instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be + /// identified (use UNAUTHENTICATED instead for those errors). + /// + PermissionDenied = 7, + + /// + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + /// is out of space. + /// + ResourceExhausted = 8, + + /// + /// Operation was rejected because the system is not in a state required for the operation's + /// execution. For example, directory to be deleted may be non-empty, an rmdir operation is + /// applied to a non-directory, etc. + /// A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION, + /// ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call. + /// (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a + /// read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory + /// is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// + FailedPrecondition = 9, + + /// + /// The operation was aborted, typically due to a concurrency issue like sequencer check + /// failures, transaction aborts, etc. + /// + Aborted = 10, + + /// + /// Operation was attempted past the valid range. E.g., seeking or reading past end of file. + /// + /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system + /// state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to + /// read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if + /// asked to read from an offset past the current file size. + /// + /// There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend + /// using OUT_OF_RANGE (the more specific error) when it applies so that callers who are + /// iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are + /// done. + /// + OutOfRange = 11, + + /// + /// Operation is not implemented or not supported/enabled in this service. + /// + Unimplemented = 12, + + /// + /// Internal errors. Means some invariants expected by underlying system has been broken. If you + /// see one of these errors, something is very broken. + /// + Internal = 13, + + /// + /// The service is currently unavailable. This is a most likely a transient condition and may be + /// corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE. + /// + Unavailable = 14, + + /// + /// Unrecoverable data loss or corruption. + /// + DataLoss = 15, + + /// + /// The request does not have valid authentication credentials for the operation. + /// + Unauthenticated = 16, +} diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs index 8d58d64180206..f1a1c35f4a66e 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs @@ -2,6 +2,7 @@ // 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 #nullable enable using System; @@ -83,3 +84,4 @@ private void AddIfNormalizedKeyMatchesPrefix(Dictionary data, s private static string Normalize(string key) => key.Replace("__", ConfigurationPath.KeyDelimiter); } } +#endif diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs index 2785b2174756e..23fcdd3e0e09c 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs @@ -2,6 +2,7 @@ // 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 #nullable enable namespace Microsoft.Extensions.Configuration.EnvironmentVariables @@ -27,3 +28,4 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } } } +#endif diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs index 5b97e90ce7743..0b651f5d94823 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/src/Vendoring/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs @@ -2,6 +2,7 @@ // 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 #nullable enable using System; @@ -50,3 +51,4 @@ public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationB => builder.Add(configureSource); } } +#endif