Skip to content

Commit

Permalink
Detect Instrumentation Package and Backoff (Azure#39001)
Browse files Browse the repository at this point in the history
* AddVendorInstrumentationIfPackageNotReferenced

* PR feedback.

* event souce fix.
  • Loading branch information
rajkumar-rangaraj authored and yaotongms committed Oct 12, 2023
1 parent cc4371a commit 48dd058
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,6 +26,10 @@ namespace Azure.Monitor.OpenTelemetry.AspNetCore
/// </summary>
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";

/// <summary>
/// Configures Azure Monitor for logging, distributed tracing, and metrics.
/// </summary>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -158,5 +149,55 @@ public static OpenTelemetryBuilder UseAzureMonitor(this OpenTelemetryBuilder bui

return builder;
}

private static TracerProviderBuilder AddVendorInstrumentationIfPackageNotReferenced(this TracerProviderBuilder tracerProviderBuilder)
{
var vendorInstrumentationActions = new Dictionary<string, Action>
{
{ 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// <copyright file="GrpcTagHelper.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

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(@"^/?(?<service>.*)/(?<method>.*)$", 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;
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="statusCode">RPC status code.</param>
/// <returns>Resolved span <see cref="Status"/> for the Grpc status code.</returns>
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// <copyright file="StatusCanonicalCode.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

namespace OpenTelemetry.Instrumentation.GrpcNetClient;

/// <summary>
/// Canonical result code of span execution.
/// </summary>
/// <remarks>
/// This follows the standard GRPC codes.
/// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
/// </remarks>
internal enum StatusCanonicalCode
{
/// <summary>
/// The operation completed successfully.
/// </summary>
Ok = 0,

/// <summary>
/// The operation was cancelled (typically by the caller).
/// </summary>
Cancelled = 1,

/// <summary>
/// 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.
/// </summary>
Unknown = 2,

/// <summary>
/// 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).
/// </summary>
InvalidArgument = 3,

/// <summary>
/// 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.
/// </summary>
DeadlineExceeded = 4,

/// <summary>
/// Some requested entity (e.g., file or directory) was not found.
/// </summary>
NotFound = 5,

/// <summary>
/// Some entity that we attempted to create (e.g., file or directory) already exists.
/// </summary>
AlreadyExists = 6,

/// <summary>
/// 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).
/// </summary>
PermissionDenied = 7,

/// <summary>
/// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
/// is out of space.
/// </summary>
ResourceExhausted = 8,

/// <summary>
/// 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.
/// </summary>
FailedPrecondition = 9,

/// <summary>
/// The operation was aborted, typically due to a concurrency issue like sequencer check
/// failures, transaction aborts, etc.
/// </summary>
Aborted = 10,

/// <summary>
/// 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.
/// </summary>
OutOfRange = 11,

/// <summary>
/// Operation is not implemented or not supported/enabled in this service.
/// </summary>
Unimplemented = 12,

/// <summary>
/// Internal errors. Means some invariants expected by underlying system has been broken. If you
/// see one of these errors, something is very broken.
/// </summary>
Internal = 13,

/// <summary>
/// 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.
/// </summary>
Unavailable = 14,

/// <summary>
/// Unrecoverable data loss or corruption.
/// </summary>
DataLoss = 15,

/// <summary>
/// The request does not have valid authentication credentials for the operation.
/// </summary>
Unauthenticated = 16,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,3 +84,4 @@ private void AddIfNormalizedKeyMatchesPrefix(Dictionary<string, string?> data, s
private static string Normalize(string key) => key.Replace("__", ConfigurationPath.KeyDelimiter);
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,3 +28,4 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,3 +51,4 @@ public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationB
=> builder.Add(configureSource);
}
}
#endif

0 comments on commit 48dd058

Please sign in to comment.