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;
+ }
+ }
+}