diff --git a/opentelemetry-dotnet-contrib.slnx b/opentelemetry-dotnet-contrib.slnx index 09641c23d3..cd5441eab5 100644 --- a/opentelemetry-dotnet-contrib.slnx +++ b/opentelemetry-dotnet-contrib.slnx @@ -205,6 +205,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs index 47b7688e28..f256e0c9e8 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using static OpenTelemetry.Internal.RpcSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.AspNetCore; @@ -38,6 +39,10 @@ internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) { this.DisableUrlQueryRedaction = disableUrlQueryRedaction; } + + var rpcSemanticConvention = GetSemanticConventionOptIn(configuration); + this.EmitOldRpcAttributes = rpcSemanticConvention.HasFlag(RpcSemanticConvention.Old); + this.EmitNewRpcAttributes = rpcSemanticConvention.HasFlag(RpcSemanticConvention.New); } /// @@ -115,7 +120,7 @@ internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. /// /// - /// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md. + /// https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/rpc/rpc-spans.md. /// internal bool EnableGrpcAspNetCoreSupport { get; set; } @@ -128,4 +133,14 @@ internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) /// The redaction can be disabled by setting this property to . /// internal bool DisableUrlQueryRedaction { get; set; } + + /// + /// Gets or sets a value indicating whether the old RPC attributes should be emitted. + /// + internal bool EmitOldRpcAttributes { get; set; } + + /// + /// Gets or sets a value indicating whether the new RPC attributes should be emitted. + /// + internal bool EmitNewRpcAttributes { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index 720f970516..6d8942fe82 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -14,6 +14,11 @@ * Fix enrich methods being called multiple times. ([#4015](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4015)) +* Add support for version [1.41.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/rpc/README.md) + of the Semantic Conventions for RPC/gRPC when the `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable is set to `rpc` or `rpc/dup`. + ([#4370](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4370)) + ## 1.15.2 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index b68439c930..93f78a1424 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -295,7 +295,14 @@ public void OnStopActivity(Activity activity, object? payload) if (!string.IsNullOrEmpty(grpcMethod)) { - AddGrpcAttributes(activity, grpcMethod!, context, grpcStatusCode, hasGrpcStatusCode); + AddGrpcAttributes( + activity, + grpcMethod!, + context, + grpcStatusCode, + hasGrpcStatusCode, + this.options.EmitOldRpcAttributes, + this.options.EmitNewRpcAttributes); } } @@ -382,7 +389,14 @@ static bool TryFetchException(object? payload, [NotNullWhen(true)] out Exception } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context, int grpcStatusCode, bool validStatusCode) + private static void AddGrpcAttributes( + Activity activity, + string grpcMethod, + HttpContext context, + int grpcStatusCode, + bool validStatusCode, + bool emitOldRpcAttributes, + bool emitNewRpcAttributes) { var details = GrpcMethodCache.Get(grpcMethod); @@ -391,16 +405,33 @@ private static void AddGrpcAttributes(Activity activity, string grpcMethod, Http // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#span-name activity.DisplayName = details.DisplayName; - activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + // See the specs for old and new semantic conventions. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/rpc/rpc-spans.md + // https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/rpc/rpc-spans.md - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/rpc/rpc-spans.md - - if (context.Connection.RemoteIpAddress != null) + if (emitOldRpcAttributes) { - activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString()); + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + + if (context.Connection.RemoteIpAddress != null) + { + activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString()); + } + + activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort); } - activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort); + if (emitNewRpcAttributes) + { + activity.SetTag(SemanticConventions.AttributeRpcSystemName, GrpcTagHelper.RpcSystemGrpc); + + if (context.Connection.RemoteIpAddress != null) + { + activity.SetTag(SemanticConventions.AttributeNetworkPeerAddress, context.Connection.RemoteIpAddress.ToString()); + } + + activity.SetTag(SemanticConventions.AttributeNetworkPeerPort, context.Connection.RemotePort); + } if (validStatusCode) { @@ -409,20 +440,31 @@ private static void AddGrpcAttributes(Activity activity, string grpcMethod, Http if (details.IsParsed) { - activity.SetTag(SemanticConventions.AttributeRpcService, details.RpcService); + if (emitOldRpcAttributes) + { + activity.SetTag(SemanticConventions.AttributeRpcService, details.RpcService); + } + activity.SetTag(SemanticConventions.AttributeRpcMethod, details.RpcMethod); - // Remove the grpc.method tag added by the gRPC .NET library + // See https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/non-normative/compatibility/grpc.md#attribute-mapping activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); - - // Remove the grpc.status_code tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcStatusTagName, null); activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + activity.SetTag(GrpcTagHelper.GrpcTargetTagName, null); + } - if (validStatusCode) + if (validStatusCode) + { + if (emitOldRpcAttributes) { - // setting rpc.grpc.status_code activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, grpcStatusCode); } + + if (emitNewRpcAttributes) + { + activity.SetTag(SemanticConventions.AttributeRpcResponseStatusCode, grpcStatusCode); + } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj index eaca258988..17b3a54116 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj @@ -31,6 +31,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md index 94d4312334..f10ad11c5c 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md @@ -356,7 +356,7 @@ appBuilder.Services.AddOpenTelemetry() ``` Semantic conventions for RPC are still - [experimental](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/rpc) + [experimental](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/rpc#semantic-conventions-for-rpc) and hence the instrumentation only offers it as an experimental feature. ## Troubleshooting diff --git a/src/Shared/GrpcTagHelper.cs b/src/Shared/GrpcTagHelper.cs index 7b17a1b74f..4d12129797 100644 --- a/src/Shared/GrpcTagHelper.cs +++ b/src/Shared/GrpcTagHelper.cs @@ -13,8 +13,11 @@ internal static class GrpcTagHelper // 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. + // See https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/non-normative/compatibility/grpc.md#attribute-mapping public const string GrpcMethodTagName = "grpc.method"; + public const string GrpcStatusTagName = "grpc.status"; public const string GrpcStatusCodeTagName = "grpc.status_code"; + public const string GrpcTargetTagName = "grpc.target"; public static string? GetGrpcMethodFromActivity(Activity activity) => activity.GetTagValue(GrpcMethodTagName) as string; @@ -91,7 +94,7 @@ public static ActivityStatusCode ResolveSpanStatusForGrpcStatusCodeOnClient(int /// /// Helper method that populates span properties from RPC status code according - /// to https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/grpc.md#server. + /// to https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/rpc/grpc.md. /// This method is for server spans where only specific status codes are considered errors: /// UNKNOWN, DEADLINE_EXCEEDED, UNIMPLEMENTED, INTERNAL, UNAVAILABLE, and DATA_LOSS. /// diff --git a/src/Shared/RpcSemanticConventionHelper.cs b/src/Shared/RpcSemanticConventionHelper.cs new file mode 100644 index 0000000000..e20f5f7ee7 --- /dev/null +++ b/src/Shared/RpcSemanticConventionHelper.cs @@ -0,0 +1,84 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace OpenTelemetry.Internal; + +/// +/// Helper class for RPC Semantic Conventions. +/// +/// +/// Due to a breaking change in the semantic conventions, 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 RpcSemanticConventionHelper +{ + internal const string SemanticConventionOptInKeyName = "OTEL_SEMCONV_STABILITY_OPT_IN"; + internal static readonly char[] Separator = [',', ' ']; + + [Flags] + internal enum RpcSemanticConvention + { + /// + /// Instructs an instrumentation library to emit the old experimental RPC attributes. + /// + Old = 0x1, + + /// + /// Instructs an instrumentation library to emit the new, v1.23.0 RPC attributes. + /// + New = 0x2, + + /// + /// Instructs an instrumentation library to emit both the old and new attributes. + /// + Dupe = Old | New, + } + + public static RpcSemanticConvention GetSemanticConventionOptIn(IConfiguration configuration) + { + if (TryGetConfiguredValues(configuration, out var values)) + { + if (values.Contains("rpc/dup")) + { + return RpcSemanticConvention.Dupe; + } + else if (values.Contains("rpc")) + { + return RpcSemanticConvention.New; + } + } + + return RpcSemanticConvention.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; + } + +#pragma warning disable IDE0370 // Suppression is unnecessary + var stringValues = stringValue!.Split(separator: Separator, options: StringSplitOptions.RemoveEmptyEntries); +#pragma warning restore IDE0370 // Suppression is unnecessary + values = new HashSet(stringValues, StringComparer.OrdinalIgnoreCase); + return true; + } + catch + { + values = null; + return false; + } + } +}