Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using OpenTelemetry.Context.Propagation;
Expand All @@ -29,6 +31,7 @@ internal class HttpInListener : ListenerHandler
internal static readonly bool Net11OrGreater = Environment.Version.Major >= 11;

private const string DiagnosticSourceName = "Microsoft.AspNetCore";
private const string CreatedByInstrumentationPropertyName = "OpenTelemetry.AspNetCore.CreatedByInstrumentation";

private static readonly Func<HttpRequest, string, IEnumerable<string>> HttpRequestHeaderValuesGetter = (request, name) =>
{
Expand All @@ -42,6 +45,11 @@ internal class HttpInListener : ListenerHandler
};

private static readonly PropertyFetcher<Exception> ExceptionPropertyFetcher = new("Exception");
private static readonly object CreatedByInstrumentationMarker = new();

// Caches the display name, rpc.service, and rpc.method derived from the raw gRPC method string.
// The set of distinct gRPC method strings is bounded by the number of gRPC endpoints in the app.
private static readonly GrpcMethodDetailsCache GrpcMethodCache = new();

private readonly AspNetCoreTraceInstrumentationOptions options;
private readonly bool nativeAspNetCoreOpenTelemetryEnabled;
Expand Down Expand Up @@ -128,7 +136,7 @@ public void OnStartActivity(Activity activity, object? payload)

newOne!.TraceStateString = ctx.ActivityContext.TraceState;

newOne.SetTag("IsCreatedByInstrumentation", bool.TrueString);
newOne.SetCustomProperty(CreatedByInstrumentationPropertyName, CreatedByInstrumentationMarker);

// Starting the new activity make it the Activity.Current one.
newOne.Start();
Expand All @@ -145,36 +153,41 @@ public void OnStartActivity(Activity activity, object? payload)
// is favorable.
if (activity.IsAllDataRequested)
{
try
if (this.options.Filter is { } filter)
{
if (this.options.Filter?.Invoke(context) == false)
try
{
if (!filter(context))
{
AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName);
DisableActivity(activity);
return;
}
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex);
DisableActivity(activity);
return;
}

static void DisableActivity(Activity activity)
{
AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName);
activity.IsAllDataRequested = false;
activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded;
return;
}
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex);
activity.IsAllDataRequested = false;
activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded;
return;
}

if (!Net7OrGreater)
{
ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource);
ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server);
}

TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, request.Method);

// ASP.NET Core 10 does not support OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS so we
// still need to set the HTTP method tag so that any override by the user is honoured.
// See https://github.com/dotnet/aspnetcore/issues/65873.
TelemetryHelper.RequestDataHelper.SetHttpMethodTag(activity, request.Method);
TelemetryHelper.RequestDataHelper.SetActivityDisplayNameAndHttpMethodTag(activity, request.Method);

if (!Net10OrGreater || !this.nativeAspNetCoreOpenTelemetryEnabled)
{
Expand Down Expand Up @@ -218,13 +231,16 @@ public void OnStartActivity(Activity activity, object? payload)

activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, RequestDataHelper.GetHttpProtocolVersion(request.Protocol));

try
if (this.options.EnrichWithHttpRequest is { } enricher)
{
this.options.EnrichWithHttpRequest?.Invoke(activity, request);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex);
try
{
enricher(activity, request);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex);
}
}
}
}
Expand Down Expand Up @@ -252,41 +268,61 @@ public void OnStopActivity(Activity activity, object? payload)

activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));

if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod))
if (this.options.EnableGrpcAspNetCoreSupport && IsGrpcRequestProtocol(context.Request.Protocol))
{
AddGrpcAttributes(activity, grpcMethod, context);
// Single pass over the tag collection to retrieve both gRPC tags,
// avoiding two separate GetTagValue iterations.
string? grpcMethod = null;
int grpcStatusCode = -1;
var hasGrpcStatusCode = false;

var tagEnumerator = activity.EnumerateTagObjects();
while (tagEnumerator.MoveNext())
{
ref readonly var tag = ref tagEnumerator.Current;
if (grpcMethod is null && tag.Key == GrpcTagHelper.GrpcMethodTagName)
{
grpcMethod = tag.Value as string;
}
else if (!hasGrpcStatusCode && tag.Key == GrpcTagHelper.GrpcStatusCodeTagName)
{
hasGrpcStatusCode = int.TryParse(tag.Value as string, NumberStyles.None, CultureInfo.InvariantCulture, out grpcStatusCode);
}

if (grpcMethod is not null && hasGrpcStatusCode)
{
break;
}
}

if (!string.IsNullOrEmpty(grpcMethod))
{
AddGrpcAttributes(activity, grpcMethod!, context, grpcStatusCode, hasGrpcStatusCode);
}
}

if (activity.Status == ActivityStatusCode.Unset)
{
activity.SetStatus(SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, response.StatusCode));
}

try
{
this.options.EnrichWithHttpResponse?.Invoke(activity, response);
}
catch (Exception ex)
if (this.options.EnrichWithHttpResponse is { } enricher)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex);
try
{
enricher(activity, response);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex);
}
}
}

object? tagValue;
if (Net7OrGreater)
{
tagValue = activity.GetTagValue("IsCreatedByInstrumentation");
}
else
{
_ = activity.TryCheckFirstTag("IsCreatedByInstrumentation", out tagValue);
}

if (ReferenceEquals(tagValue, bool.TrueString))
if (ReferenceEquals(activity.GetCustomProperty(CreatedByInstrumentationPropertyName), CreatedByInstrumentationMarker))
{
// If instrumentation started a new Activity, it must
// be stopped here.
activity.SetTag("IsCreatedByInstrumentation", null);
activity.Stop();

// After the activity.Stop() code, Activity.Current becomes null.
Expand Down Expand Up @@ -322,13 +358,16 @@ public void OnException(Activity activity, object? payload)

activity.SetStatus(ActivityStatusCode.Error);

try
if (this.options.EnrichWithException is { } enricher)
{
this.options.EnrichWithException?.Invoke(activity, exc);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex);
try
{
enricher(activity, exc);
}
catch (Exception ex)
{
AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex);
}
}
}

Expand All @@ -345,19 +384,14 @@ static bool TryFetchException(object? payload, [NotNullWhen(true)] out Exception
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetGrpcMethod(Activity activity, [NotNullWhen(true)] out string? grpcMethod)
private static void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context, int grpcStatusCode, bool validStatusCode)
{
grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity);
return !string.IsNullOrEmpty(grpcMethod);
}
var details = GrpcMethodCache.Get(grpcMethod);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context)
{
// The RPC semantic conventions indicate the span name
// should not have a leading forward slash.
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#span-name
activity.DisplayName = grpcMethod.TrimStart('/');
activity.DisplayName = details.DisplayName;

activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc);

Expand All @@ -370,27 +404,26 @@ private static void AddGrpcAttributes(Activity activity, string grpcMethod, Http

activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort);

var validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out var status);
if (validConversion)
if (validStatusCode)
{
activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCodeOnServer(status));
activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCodeOnServer(grpcStatusCode));
}

if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod))
if (details.IsParsed)
{
activity.SetTag(SemanticConventions.AttributeRpcService, rpcService);
activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod);
activity.SetTag(SemanticConventions.AttributeRpcService, details.RpcService);
activity.SetTag(SemanticConventions.AttributeRpcMethod, details.RpcMethod);

// Remove the grpc.method tag added by the gRPC .NET library
activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null);

// Remove the grpc.status_code tag added by the gRPC .NET library
activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null);

if (validConversion)
if (validStatusCode)
{
// setting rpc.grpc.status_code
activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status);
activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, grpcStatusCode);
}
}
}
Expand Down Expand Up @@ -419,4 +452,52 @@ private static bool AspNetCoreHasNativeOpenTelemetryTags()
// see https://github.com/dotnet/aspnetcore/blob/655f41d52f2fc75992eac41496b8e9cc119e1b54/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L59-L67.
return Net11OrGreater;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsGrpcRequestProtocol(string protocol) =>
protocol == "HTTP/2" || protocol == "HTTP/3";

private readonly struct GrpcMethodDetails
{
public GrpcMethodDetails(string displayName, string? rpcService, string? rpcMethod, bool isParsed)
{
this.DisplayName = displayName;
this.RpcService = rpcService;
this.RpcMethod = rpcMethod;
this.IsParsed = isParsed;
}

public readonly string DisplayName { get; }

public readonly string? RpcService { get; }

public readonly string? RpcMethod { get; }

public readonly bool IsParsed { get; }
}

private sealed class GrpcMethodDetailsCache
{
private const int MaxCacheSize = 512;
private readonly ConcurrentDictionary<string, GrpcMethodDetails> cache = new();

public GrpcMethodDetails Get(string grpcMethod)
{
if (this.cache.TryGetValue(grpcMethod, out var details))
{
return details;
}

// If the cache has reached its maximum size, just create a value without caching
return this.cache.Count >= MaxCacheSize ? Create(grpcMethod) : this.cache.GetOrAdd(grpcMethod, Create);
}

private static GrpcMethodDetails Create(string method)
{
var displayName = method.Length > 0 && method[0] == '/' ? method.Substring(1) : method;
var isParsed = GrpcTagHelper.TryParseRpcServiceAndRpcMethod(method, out var serviceName, out var methodName);

return new(displayName, isParsed ? serviceName : null, isParsed ? methodName : null, isParsed);
}
}
}
5 changes: 5 additions & 0 deletions src/Shared/GrpcStatusCanonicalCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,9 @@ internal enum GrpcStatusCanonicalCode
/// The request does not have valid authentication credentials for the operation.
/// </summary>
Unauthenticated = 16,

/// <summary>
/// The maximum valid GRPC status code.
/// </summary>
MaxValue = Unauthenticated, // Update this value if new status codes are added.
Comment thread
martincostello marked this conversation as resolved.
}
Loading
Loading