From 1bbaf049d5eec700b60df71ebc49122697f5773a Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 2 Jul 2026 10:28:22 +0100 Subject: [PATCH 1/6] [ElasticsearchClient] Update Semantic Conventions Update the Semantic Conventions to v1.42.0 when `OTEL_SEMCONV_STABILITY_OPT_IN=database` is enabled. Resolves #2302. Resolves #2976. Contributes to #4064. --- .../CHANGELOG.md | 34 +++ ...asticsearchClientInstrumentationOptions.cs | 27 +++ ...searchRequestPipelineDiagnosticListener.cs | 118 +++++++++-- ...Instrumentation.ElasticsearchClient.csproj | 6 +- .../README.md | 4 +- ...searchClientInstrumentationOptionsTests.cs | 46 ++++ .../ElasticsearchClientTests.cs | 200 ++++++++++++++++++ 7 files changed, 419 insertions(+), 16 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientInstrumentationOptionsTests.cs diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md index 86bd7faecb..ac358cc539 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md @@ -8,6 +8,40 @@ * Updated OpenTelemetry core component version(s) to `1.16.0`. ([#4487](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4487)) +* Updated the database semantic conventions to + [v1.42.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md) + of the Semantic Conventions for Elasticsearch client operations. The new + attributes can be opted in to by setting the `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable. This allows for a transition period for users to + experiment with the new semantic conventions and adapt as necessary. The + environment variable supports the following values: + * `database` - emit the new, frozen (proposed for stable) database attributes, + and stop emitting the old experimental database attributes that the + instrumentation emitted previously. + * `database/dup` - emit both the old and the frozen (proposed for stable) + database attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same database semantic conventions that were emitted in + the previous version. + * Note: this option will be removed after the new database semantic + conventions are marked stable. At which time this instrumentation can receive + a stable release, and the old database semantic conventions will no longer be + supported. Refer to the + [specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md) + for more information. + + When the new attributes are emitted: + * `net.peer.ip` and `net.peer.name` are replaced by `server.address`. + * `net.peer.port` is replaced by `server.port`. + * `db.system` is replaced by `db.system.name`. + * `db.name` is replaced by `db.collection.name`. + * `db.method` is replaced by `http.request.method`. + * `db.statement` is replaced by `db.query.text`. + * `http.status_code` is replaced by `db.response.status_code`. + * `error.type` is emitted when the operation fails. + + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ## 1.15.1-beta.1 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/ElasticsearchClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/ElasticsearchClientInstrumentationOptions.cs index a5bf693622..d3101ff11a 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/ElasticsearchClientInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/ElasticsearchClientInstrumentationOptions.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using Microsoft.Extensions.Configuration; +using static OpenTelemetry.Internal.DatabaseSemanticConventionHelper; namespace OpenTelemetry.Instrumentation.ElasticsearchClient; @@ -10,6 +12,21 @@ namespace OpenTelemetry.Instrumentation.ElasticsearchClient; /// public class ElasticsearchClientInstrumentationOptions { + /// + /// Initializes a new instance of the class. + /// + public ElasticsearchClientInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + { + } + + internal ElasticsearchClientInstrumentationOptions(IConfiguration configuration) + { + var databaseSemanticConvention = GetSemanticConventionOptIn(configuration); + this.EmitOldAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.Old); + this.EmitNewAttributes = databaseSemanticConvention.HasFlag(DatabaseSemanticConvention.New); + } + /// /// Gets or sets a value indicating whether down stream instrumentation (HttpClient) is suppressed (disabled). /// @@ -41,4 +58,14 @@ public class ElasticsearchClientInstrumentationOptions /// The type of this object depends on the event, which is given by the above parameter. /// public Action? Enrich { get; set; } + + /// + /// Gets or sets a value indicating whether the old database attributes should be emitted. + /// + internal bool EmitOldAttributes { get; set; } + + /// + /// Gets or sets a value indicating whether the new database attributes should be emitted. + /// + internal bool EmitNewAttributes { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs index 5a59f81d7a..8a66369689 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Globalization; #if NETFRAMEWORK using System.Net.Http; #endif @@ -27,10 +28,17 @@ internal partial class ElasticsearchRequestPipelineDiagnosticListener : Listener #pragma warning disable IDE0370 // Suppression is unnecessary internal static readonly string ActivitySourceName = AssemblyName.Name!; #pragma warning restore IDE0370 // Suppression is unnecessary - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Assembly.GetPackageVersion()); + + internal static readonly Version SemanticConventionsVersion = new(1, 23, 0); + internal static readonly ActivitySource ActivitySource = ActivitySourceFactory.Create(SemanticConventionsVersion); private const string RequestRegexPattern = @"\n# Request:\r?\n(\{.*)\n# Response"; + private static readonly Version SemanticConventionsVersionNew = new(1, 42, 0); + private static readonly ActivitySource ActivitySourceNew = ActivitySourceFactory.Create(SemanticConventionsVersionNew); + + private static readonly ActivitySource ActivitySourceBoth = ActivitySourceFactory.Create(null); + #if !NET private static readonly Regex ParseRequest = new(RequestRegexPattern, RegexOptions.Compiled | RegexOptions.Singleline); #endif @@ -195,7 +203,15 @@ private void OnStartActivity(Activity activity, object? payload) // remove sensitive information like user and password information uri = UriHelper.ScrubUserInfo(uri); - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + var emitOldAttributes = this.options.EmitOldAttributes; + var emitNewAttributes = this.options.EmitNewAttributes; + + var activitySource = + emitOldAttributes && emitNewAttributes ? ActivitySourceBoth : + emitNewAttributes ? ActivitySourceNew : + ActivitySource; + + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySource); ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); var method = this.methodFetcher.Fetch(payload); @@ -207,31 +223,77 @@ private void OnStartActivity(Activity activity, object? payload) var elasticIndex = GetElasticIndex(uri); activity.DisplayName = GetDisplayName(activity, method, elasticIndex); - activity.SetTag(SemanticConventions.AttributeDbSystem, DatabaseSystemName); + + if (emitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbSystem, DatabaseSystemName); + } + + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbSystemName, DatabaseSystemName); + } if (elasticIndex != null) { - activity.SetTag(SemanticConventions.AttributeDbName, elasticIndex); + if (emitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbName, elasticIndex); + } + + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbCollectionName, elasticIndex); + } } var uriHostNameType = Uri.CheckHostName(uri.Host); - if (uriHostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6) + var isIpAddress = uriHostNameType is UriHostNameType.IPv4 or UriHostNameType.IPv6; + + if (emitOldAttributes) { - activity.SetTag(SemanticConventions.AttributeNetPeerIp, uri.Host); + activity.SetTag(isIpAddress ? SemanticConventions.AttributeNetPeerIp : SemanticConventions.AttributeNetPeerName, uri.Host); } - else + + if (emitNewAttributes) { - activity.SetTag(SemanticConventions.AttributeNetPeerName, uri.Host); + activity.SetTag(SemanticConventions.AttributeServerAddress, uri.Host); + + if (isIpAddress) + { + activity.SetTag(SemanticConventions.AttributeNetworkPeerAddress, uri.Host); + } } if (uri.Port > 0) { - activity.SetTag(SemanticConventions.AttributeNetPeerPort, uri.Port); + if (emitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeNetPeerPort, uri.Port); + } + + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeServerPort, uri.Port); + + if (isIpAddress) + { + activity.SetTag(SemanticConventions.AttributeNetworkPeerPort, uri.Port); + } + } } if (method != null) { - activity.SetTag(AttributeDbMethod, method.ToString()); + if (emitOldAttributes) + { + activity.SetTag(AttributeDbMethod, method.ToString()); + } + + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, method.ToString()); + } } activity.SetTag(SemanticConventions.AttributeUrlFull, uri.OriginalString); @@ -251,12 +313,24 @@ private void OnStopActivity(Activity activity, object? payload) { if (activity.IsAllDataRequested) { + var emitOldAttributes = this.options.EmitOldAttributes; + var emitNewAttributes = this.options.EmitNewAttributes; + var statusCode = this.httpStatusFetcher.Fetch(payload); - activity.SetStatus(SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, statusCode.GetValueOrDefault())); + var activityStatus = SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, statusCode.GetValueOrDefault()); + activity.SetStatus(activityStatus); if (statusCode.HasValue) { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)statusCode); + if (emitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)statusCode); + } + + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbResponseStatusCode, statusCode.Value.ToString(CultureInfo.InvariantCulture)); + } } var debugInformation = this.debugInformationFetcher.Fetch(payload); @@ -268,7 +342,15 @@ private void OnStopActivity(Activity activity, object? payload) dbStatement = dbStatement.Substring(0, this.options.MaxDbStatementLength); } - activity.SetTag(SemanticConventions.AttributeDbStatement, dbStatement); + if (emitOldAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, dbStatement); + } + + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeDbQueryText, dbStatement); + } } var originalException = this.originalExceptionFetcher.Fetch(payload); @@ -276,6 +358,11 @@ private void OnStopActivity(Activity activity, object? payload) { activity.SetCustomProperty(ExceptionCustomPropertyName, originalException); + if (emitNewAttributes) + { + activity.SetTag(SemanticConventions.AttributeErrorType, originalException.GetType().FullName); + } + var failureReason = this.failureReasonFetcher.Fetch(originalException); if (failureReason != null) { @@ -303,6 +390,11 @@ private void OnStopActivity(Activity activity, object? payload) } } } + else if (emitNewAttributes && activityStatus == ActivityStatusCode.Error && statusCode.HasValue) + { + // No exception was thrown, but the response indicates an error (4xx/5xx). + activity.SetTag(SemanticConventions.AttributeErrorType, statusCode.Value.ToString(CultureInfo.InvariantCulture)); + } try { diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/OpenTelemetry.Instrumentation.ElasticsearchClient.csproj b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/OpenTelemetry.Instrumentation.ElasticsearchClient.csproj index 95f5bf0341..45b2299b54 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/OpenTelemetry.Instrumentation.ElasticsearchClient.csproj +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/OpenTelemetry.Instrumentation.ElasticsearchClient.csproj @@ -27,14 +27,18 @@ + + + - + + diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md index 93df8b5bd0..8e73ef1fa3 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md @@ -18,9 +18,9 @@ and collects traces about outgoing requests. > [!NOTE] > This component is based on the OpenTelemetry semantic conventions for -[metrics](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/elasticsearch.md) +[metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md#metrics) and -[traces](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/elasticsearch.md). +[traces](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md#spans). These conventions are [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), and hence, this package is a diff --git a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientInstrumentationOptionsTests.cs b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientInstrumentationOptionsTests.cs new file mode 100644 index 0000000000..47bb33eaaa --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientInstrumentationOptionsTests.cs @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.ElasticsearchClient.Tests; + +public class ElasticsearchClientInstrumentationOptionsTests +{ + [Fact] + public void ShouldEmitOldAttributesWhenStabilityOptInIsNotSpecified() + { + var configuration = new ConfigurationBuilder().Build(); + var options = new ElasticsearchClientInstrumentationOptions(configuration); + + Assert.True(options.EmitOldAttributes); + Assert.False(options.EmitNewAttributes); + } + + [Fact] + public void ShouldEmitNewAttributesWhenStabilityOptInIsDatabase() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection([new(DatabaseSemanticConventionHelper.SemanticConventionOptInKeyName, "database")]) + .Build(); + + var options = new ElasticsearchClientInstrumentationOptions(configuration); + + Assert.False(options.EmitOldAttributes); + Assert.True(options.EmitNewAttributes); + } + + [Fact] + public void ShouldEmitBothAttributesWhenStabilityOptInIsDatabaseDup() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection([new(DatabaseSemanticConventionHelper.SemanticConventionOptInKeyName, "database/dup")]) + .Build(); + + var options = new ElasticsearchClientInstrumentationOptions(configuration); + + Assert.True(options.EmitOldAttributes); + Assert.True(options.EmitNewAttributes); + } +} diff --git a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs index 855fb270e8..01ca57d9bc 100644 --- a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs @@ -853,4 +853,204 @@ public async Task ShouldRemoveSensitiveInformation() Assert.DoesNotContain("sensitive", dbUrl); Assert.Contains("REDACTED:REDACTED", dbUrl); } + + [Fact] + public async Task EmitsNewSemanticConventionsWhenOptedIn() + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + + var client = new ElasticClient(new ConnectionSettings(new InMemoryConnection()).DefaultIndex("customer")); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddElasticsearchClientInstrumentation(o => + { + o.EmitOldAttributes = false; + o.EmitNewAttributes = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var response = await client.GetAsync("123"); + + Assert.NotNull(response); + Assert.True(response.ApiCall.Success); + } + + Assert.Single(exportedItems); + var searchActivity = exportedItems[0]; + + // New attributes are emitted. + Assert.Equal("elasticsearch", searchActivity.GetTagValue(SemanticConventions.AttributeDbSystemName)); + Assert.Equal("customer", searchActivity.GetTagValue(SemanticConventions.AttributeDbCollectionName)); + Assert.Equal("localhost", searchActivity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal("GET", searchActivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeUrlFull)); + Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeDbQueryText)); + + // localhost is a hostname (not an IP), so network.peer.* are not set. + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetworkPeerAddress)); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetworkPeerPort)); + + // Old attributes are not emitted. + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeDbSystem)); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeDbName)); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + Assert.Null(searchActivity.GetTagValue("db.method")); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeDbStatement)); + Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); + } + + [Fact] + public async Task EmitsBothSemanticConventionsWhenOptedIn() + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + + var client = new ElasticClient(new ConnectionSettings(new InMemoryConnection()).DefaultIndex("customer")); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddElasticsearchClientInstrumentation(o => + { + o.EmitOldAttributes = true; + o.EmitNewAttributes = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var response = await client.GetAsync("123"); + + Assert.NotNull(response); + Assert.True(response.ApiCall.Success); + } + + Assert.Single(exportedItems); + var searchActivity = exportedItems[0]; + + // Old attributes. + Assert.Equal("elasticsearch", searchActivity.GetTagValue(SemanticConventions.AttributeDbSystem)); + Assert.Equal("customer", searchActivity.GetTagValue(SemanticConventions.AttributeDbName)); + Assert.Equal("localhost", searchActivity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + Assert.Equal("GET", searchActivity.GetTagValue("db.method")); + Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeDbStatement)); + + // New attributes. + Assert.Equal("elasticsearch", searchActivity.GetTagValue(SemanticConventions.AttributeDbSystemName)); + Assert.Equal("customer", searchActivity.GetTagValue(SemanticConventions.AttributeDbCollectionName)); + Assert.Equal("localhost", searchActivity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal("GET", searchActivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeDbQueryText)); + } + + [Fact] + public async Task EmitsNetworkPeerAddressForIpAddressWhenNewSemanticConventionsOptedIn() + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + + var client = new ElasticClient(new ConnectionSettings( + new SingleNodeConnectionPool(new Uri("http://127.0.0.1:9200")), new InMemoryConnection()).DefaultIndex("customer")); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddElasticsearchClientInstrumentation(o => + { + o.EmitOldAttributes = false; + o.EmitNewAttributes = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var response = await client.GetAsync("123"); + + Assert.NotNull(response); + Assert.True(response.ApiCall.Success); + } + + Assert.Single(exportedItems); + var searchActivity = exportedItems[0]; + + Assert.Equal("127.0.0.1", searchActivity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal("127.0.0.1", searchActivity.GetTagValue(SemanticConventions.AttributeNetworkPeerAddress)); + Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeNetworkPeerPort)); + } + + [Fact] + public async Task EmitsErrorTypeFromStatusCodeWhenNewSemanticConventionsOptedIn() + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + + var client = new ElasticClient(new ConnectionSettings(new InMemoryConnection(null, statusCode: 404)).DefaultIndex("customer")); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddElasticsearchClientInstrumentation(o => + { + o.EmitOldAttributes = false; + o.EmitNewAttributes = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var response = await client.GetAsync("123"); + + Assert.NotNull(response); + } + + Assert.Single(exportedItems); + var searchActivity = exportedItems[0]; + + Assert.Equal(ActivityStatusCode.Error, searchActivity.Status); + Assert.Equal("404", searchActivity.GetTagValue(SemanticConventions.AttributeDbResponseStatusCode)); + Assert.Equal("404", searchActivity.GetTagValue(SemanticConventions.AttributeErrorType)); + } + + [Fact] + public async Task EmitsErrorTypeFromExceptionWhenNewSemanticConventionsOptedIn() + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + + var connection = new InMemoryConnection(Encoding.UTF8.GetBytes("{}"), statusCode: 500, exception: new ElasticsearchClientException("Boom")); + var client = new ElasticClient(new ConnectionSettings(connection).DefaultIndex("customer").EnableDebugMode()); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddElasticsearchClientInstrumentation(o => + { + o.EmitOldAttributes = false; + o.EmitNewAttributes = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var searchResponse = await client.SearchAsync(s => s.Query(q => q.Bool(b => b.Must(m => m.Term(f => f.Id, "123"))))); + Assert.NotNull(searchResponse); + Assert.False(searchResponse.ApiCall.Success); + + var expectedErrorType = searchResponse.ApiCall.OriginalException?.GetType().FullName; + Assert.NotNull(expectedErrorType); + + Assert.Single(exportedItems); + var searchActivity = exportedItems[0]; + + Assert.Equal(ActivityStatusCode.Error, searchActivity.Status); + Assert.Equal("500", searchActivity.GetTagValue(SemanticConventions.AttributeDbResponseStatusCode)); + Assert.Equal(expectedErrorType, searchActivity.GetTagValue(SemanticConventions.AttributeErrorType)); + } + } } From bdd47cd67ea0a8d5537e6334f9857220896c1bab Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 2 Jul 2026 10:35:48 +0100 Subject: [PATCH 2/6] [ElasticsearchClient] Update CHANGELOG Add PR number. --- .../CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md index ac358cc539..7ec6335a62 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md @@ -40,7 +40,7 @@ * `http.status_code` is replaced by `db.response.status_code`. * `error.type` is emitted when the operation fails. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ([#4635](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4635)) ## 1.15.1-beta.1 From 1ef50f551f5847fa8f69bc14b25d62f2e843980b Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 2 Jul 2026 10:43:09 +0100 Subject: [PATCH 3/6] [ElasticsearchClient] Avoid double operation Only call `ToString()` for the method name once. --- .../ElasticsearchRequestPipelineDiagnosticListener.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs index 8a66369689..d4f6fe4066 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs @@ -285,14 +285,16 @@ private void OnStartActivity(Activity activity, object? payload) if (method != null) { + var methodName = method.ToString(); + if (emitOldAttributes) { - activity.SetTag(AttributeDbMethod, method.ToString()); + activity.SetTag(AttributeDbMethod, methodName); } if (emitNewAttributes) { - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, method.ToString()); + activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, methodName); } } From 9cf142851017e5adadf853e98493a3fbddd58905 Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 2 Jul 2026 11:02:43 +0100 Subject: [PATCH 4/6] [ElasticsearchClient] Derive db.operation.name - Derive the `db.operation.name` attribute from the ElasticSearch operation. - Set the span's DisplayName. - Refactor tests. --- .../CHANGELOG.md | 2 + ...searchRequestPipelineDiagnosticListener.cs | 118 +++++++++++++++++- .../ElasticsearchClientTests.cs | 110 ++++++++-------- 3 files changed, 174 insertions(+), 56 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md index 7ec6335a62..863325bc4b 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md @@ -39,6 +39,8 @@ * `db.statement` is replaced by `db.query.text`. * `http.status_code` is replaced by `db.response.status_code`. * `error.type` is emitted when the operation fails. + * `db.operation.name` is emitted, derived from the request URL. + * The span name is `{db.operation.name} {db.collection.name}`. ([#4635](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4635)) diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs index d4f6fe4066..1d9cd7c168 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs @@ -44,6 +44,42 @@ internal partial class ElasticsearchRequestPipelineDiagnosticListener : Listener #endif private static readonly ConcurrentDictionary MethodNameCache = new(); + // Some Elasticsearch endpoints are namespaced with a fixed, low-cardinality sub-action, e.g. "_cat/aliases" or + // "_eql/search". Unlike e.g. "_snapshot/{repository}" or "_tasks/{task_id}", where the segment following the + // namespace is a user-supplied resource identifier, these sub-actions are drawn from a small known vocabulary + // and are safe to combine into `db.operation.name` (e.g. "cat.aliases") without risking high cardinality. + private static readonly Dictionary> NamespacedSubActions = new(StringComparer.Ordinal) + { + ["cat"] = new(StringComparer.Ordinal) + { + "aliases", + "allocation", + "component_templates", + "count", + "fielddata", + "health", + "indices", + "master", + "ml", + "nodeattrs", + "nodes", + "pending_tasks", + "plugins", + "recovery", + "repositories", + "segments", + "shards", + "snapshots", + "tasks", + "templates", + "thread_pool", + "transforms", + }, + ["eql"] = new(StringComparer.Ordinal) { "search" }, + ["graph"] = new(StringComparer.Ordinal) { "explore" }, + ["sql"] = new(StringComparer.Ordinal) { "close", "delete_async", "get_async", "get_async_status", "translate" }, + }; + private readonly ElasticsearchClientInstrumentationOptions options; private readonly MultiTypePropertyFetcher uriFetcher = new("Uri"); private readonly MultiTypePropertyFetcher methodFetcher = new("Method"); @@ -76,14 +112,25 @@ public override void OnEventWritten(string name, object? payload) } } - private static string GetDisplayName(Activity activity, object? method, string? elasticType = null) + private static string GetDisplayName(Activity activity, object? method, string? elasticType, string? dbOperationName, bool useNewSpanNameFormat) { switch (activity.OperationName) { case "Ping": return "Elasticsearch Ping"; + case "CallElasticsearch" when method != null: { + if (useNewSpanNameFormat) + { + // Per the stable database semantic conventions, the span name is + // "{db.operation.name} {target}", falling back to just the target or operation name + // when the other is not available. + return dbOperationName == null + ? elasticType ?? method.ToString()! + : elasticType == null ? dbOperationName : $"{dbOperationName} {elasticType}"; + } + var methodName = MethodNameCache.GetOrAdd(method, $"Elasticsearch {method}"); return elasticType == null ? methodName : $"{methodName} {elasticType}"; } @@ -135,6 +182,67 @@ private static string GetDisplayName(Activity activity, object? method, string? return elasticType; } + private static string? GetDbOperationName(Uri uri, object? method) + { + // The `_doc`/`_create` endpoints map to different logical operations depending on the HTTP method used. + var segments = uri.Segments; + string? endpoint = null; + string? @namespace = null; + + for (var i = 1; i < segments.Length; i++) + { + var segment = Uri.UnescapeDataString(segments[i]).TrimEnd('/'); + + if (segment.Length == 0) + { + continue; + } + + if (segment[0] != '_') + { + // Only combine with the following segment when it is a known, fixed sub-action for the current + // namespace (see NamespacedSubActions); otherwise, it is an index/id/other identifier and is + // ignored to avoid introducing unbounded cardinality into `db.operation.name`. + if (endpoint != null && + @namespace == null && + NamespacedSubActions.TryGetValue(endpoint, out var subActions) && + subActions.Contains(segment)) + { + @namespace = endpoint; + endpoint = segment; + } + + continue; + } + + endpoint = segment.Substring(1); + @namespace = null; + } + + if (endpoint == null) + { + return null; + } + + var methodName = method?.ToString(); + + if (string.Equals(endpoint, "doc", StringComparison.Ordinal) || + string.Equals(endpoint, "create", StringComparison.Ordinal)) + { + endpoint = methodName?.ToUpperInvariant() switch + { + "GET" => "get", + "HEAD" => "exists", + "PUT" => "index", + "POST" => "create", + "DELETE" => "delete", + _ => endpoint, + }; + } + + return @namespace == null ? endpoint : $"{@namespace}.{endpoint}"; + } + #if NET [GeneratedRegex(RequestRegexPattern, RegexOptions.Singleline)] private static partial Regex RequestParser(); @@ -222,7 +330,8 @@ private void OnStartActivity(Activity activity, object? payload) } var elasticIndex = GetElasticIndex(uri); - activity.DisplayName = GetDisplayName(activity, method, elasticIndex); + var dbOperationName = emitNewAttributes ? GetDbOperationName(uri, method) : null; + activity.DisplayName = GetDisplayName(activity, method, elasticIndex, dbOperationName, emitNewAttributes && !emitOldAttributes); if (emitOldAttributes) { @@ -232,6 +341,11 @@ private void OnStartActivity(Activity activity, object? payload) if (emitNewAttributes) { activity.SetTag(SemanticConventions.AttributeDbSystemName, DatabaseSystemName); + + if (dbOperationName != null) + { + activity.SetTag(SemanticConventions.AttributeDbOperationName, dbOperationName); + } } if (elasticIndex != null) diff --git a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs index 01ca57d9bc..6d3a269086 100644 --- a/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.ElasticsearchClient.Tests/ElasticsearchClientTests.cs @@ -44,9 +44,7 @@ public async Task CanCaptureGetById() Assert.Empty(failed); } - Assert.Single(exportedItems); - - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -67,8 +65,6 @@ public async Task CanCaptureGetById() Assert.Contains("Successful (200) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -97,9 +93,7 @@ public async Task CanCaptureGetByIdNotFound() Assert.Empty(failed); } - Assert.Single(exportedItems); - - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -120,8 +114,6 @@ public async Task CanCaptureGetByIdNotFound() Assert.Contains("Successful (404) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Error, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -150,8 +142,7 @@ public async Task CanCaptureSearchCall() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -172,8 +163,6 @@ public async Task CanCaptureSearchCall() Assert.Contains("Successful (200) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -382,8 +371,7 @@ public async Task CanCaptureSearchCallWithDebugMode() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -404,8 +392,6 @@ public async Task CanCaptureSearchCallWithDebugMode() Assert.Contains("Successful (200) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -434,8 +420,7 @@ public async Task CanCaptureSearchCallWithParseAndFormatRequestOption() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -471,8 +456,6 @@ public async Task CanCaptureSearchCallWithParseAndFormatRequestOption() debugInfo.Replace("\r\n", "\n")); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -501,8 +484,7 @@ public async Task CanCaptureSearchCallWithoutDebugMode() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -523,8 +505,6 @@ public async Task CanCaptureSearchCallWithoutDebugMode() Assert.DoesNotContain("123", debugInfo); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -553,8 +533,7 @@ public async Task CanCaptureMultipleIndiceSearchCall() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -575,8 +554,6 @@ public async Task CanCaptureMultipleIndiceSearchCall() Assert.Contains("Successful (200) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -606,8 +583,7 @@ public async Task CanCaptureElasticsearchClientException() Assert.NotEmpty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -628,8 +604,6 @@ public async Task CanCaptureElasticsearchClientException() Assert.Contains("Unsuccessful (500) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Error, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -658,8 +632,7 @@ public async Task CanCaptureCatRequest() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(parent.TraceId, searchActivity.Context.TraceId); Assert.Equal(parent.SpanId, searchActivity.ParentSpanId); @@ -680,8 +653,6 @@ public async Task CanCaptureCatRequest() Assert.Contains("Successful (200) low level call", debugInfo); Assert.Equal(ActivityStatusCode.Unset, searchActivity.Status); - - // Assert.Equal(expectedResource, searchActivity.GetResource()); } [Fact] @@ -778,8 +749,7 @@ public async Task DbStatementIsNotDisplayedWhenSetDbStatementForRequestIsFalse() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); var tags = searchActivity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeDbStatement)); @@ -811,8 +781,7 @@ public async Task DbStatementIsDisplayedWhenSetDbStatementForRequestIsUsingTheDe Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); var tags = searchActivity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeDbStatement)); @@ -845,8 +814,7 @@ public async Task ShouldRemoveSensitiveInformation() Assert.Empty(failed); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); var dbUrl = (string?)searchActivity.GetTagValue(SemanticConventions.AttributeUrlFull); @@ -879,18 +847,21 @@ public async Task EmitsNewSemanticConventionsWhenOptedIn() Assert.True(response.ApiCall.Success); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); // New attributes are emitted. Assert.Equal("elasticsearch", searchActivity.GetTagValue(SemanticConventions.AttributeDbSystemName)); Assert.Equal("customer", searchActivity.GetTagValue(SemanticConventions.AttributeDbCollectionName)); + Assert.Equal("get", searchActivity.GetTagValue(SemanticConventions.AttributeDbOperationName)); Assert.Equal("localhost", searchActivity.GetTagValue(SemanticConventions.AttributeServerAddress)); Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeServerPort)); Assert.Equal("GET", searchActivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeUrlFull)); Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeDbQueryText)); + // Span name follows the stable convention: "{db.operation.name} {db.collection.name}". + Assert.Equal("get customer", searchActivity.DisplayName); + // localhost is a hostname (not an IP), so network.peer.* are not set. Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetworkPeerAddress)); Assert.Null(searchActivity.GetTagValue(SemanticConventions.AttributeNetworkPeerPort)); @@ -931,8 +902,7 @@ public async Task EmitsBothSemanticConventionsWhenOptedIn() Assert.True(response.ApiCall.Success); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); // Old attributes. Assert.Equal("elasticsearch", searchActivity.GetTagValue(SemanticConventions.AttributeDbSystem)); @@ -945,10 +915,14 @@ public async Task EmitsBothSemanticConventionsWhenOptedIn() // New attributes. Assert.Equal("elasticsearch", searchActivity.GetTagValue(SemanticConventions.AttributeDbSystemName)); Assert.Equal("customer", searchActivity.GetTagValue(SemanticConventions.AttributeDbCollectionName)); + Assert.Equal("get", searchActivity.GetTagValue(SemanticConventions.AttributeDbOperationName)); Assert.Equal("localhost", searchActivity.GetTagValue(SemanticConventions.AttributeServerAddress)); Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeServerPort)); Assert.Equal("GET", searchActivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); Assert.NotNull(searchActivity.GetTagValue(SemanticConventions.AttributeDbQueryText)); + + // Span name retains the legacy format while old attributes are still being emitted. + Assert.Equal("Elasticsearch GET customer", searchActivity.DisplayName); } [Fact] @@ -977,8 +951,7 @@ public async Task EmitsNetworkPeerAddressForIpAddressWhenNewSemanticConventionsO Assert.True(response.ApiCall.Success); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal("127.0.0.1", searchActivity.GetTagValue(SemanticConventions.AttributeServerAddress)); Assert.Equal(9200, searchActivity.GetTagValue(SemanticConventions.AttributeServerPort)); @@ -1010,8 +983,7 @@ public async Task EmitsErrorTypeFromStatusCodeWhenNewSemanticConventionsOptedIn( Assert.NotNull(response); } - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(ActivityStatusCode.Error, searchActivity.Status); Assert.Equal("404", searchActivity.GetTagValue(SemanticConventions.AttributeDbResponseStatusCode)); @@ -1045,12 +1017,42 @@ public async Task EmitsErrorTypeFromExceptionWhenNewSemanticConventionsOptedIn() var expectedErrorType = searchResponse.ApiCall.OriginalException?.GetType().FullName; Assert.NotNull(expectedErrorType); - Assert.Single(exportedItems); - var searchActivity = exportedItems[0]; + var searchActivity = Assert.Single(exportedItems); Assert.Equal(ActivityStatusCode.Error, searchActivity.Status); Assert.Equal("500", searchActivity.GetTagValue(SemanticConventions.AttributeDbResponseStatusCode)); Assert.Equal(expectedErrorType, searchActivity.GetTagValue(SemanticConventions.AttributeErrorType)); } } + + [Fact] + public async Task EmitsDbOperationNameForSearchWhenNewSemanticConventionsOptedIn() + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + + var client = new ElasticClient(new ConnectionSettings(new InMemoryConnection()).DefaultIndex("customer")); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddElasticsearchClientInstrumentation(o => + { + o.EmitOldAttributes = false; + o.EmitNewAttributes = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var response = await client.SearchAsync(s => s.Query(q => q.Bool(b => b.Must(m => m.Term(f => f.Id, "123"))))); + + Assert.NotNull(response); + Assert.True(response.ApiCall.Success); + } + + var searchActivity = Assert.Single(exportedItems); + + Assert.Equal("search", searchActivity.GetTagValue(SemanticConventions.AttributeDbOperationName)); + Assert.Equal("search customer", searchActivity.DisplayName); + } } From 5789b201c4cd52a5b72b2b81e06480306ef00bc2 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 8 Jul 2026 10:11:39 +0100 Subject: [PATCH 5/6] [ElasticsearchClient] Update Semantic Conventions to v1.43.0 Bump version to v1.43.0. --- .../CHANGELOG.md | 4 ++-- .../ElasticsearchRequestPipelineDiagnosticListener.cs | 2 +- .../README.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md index 1d056109d0..bd9341abdc 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md @@ -12,7 +12,7 @@ ([#4637](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4637)) * Updated the database semantic conventions to - [v1.42.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md) + [v1.43.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/db/elasticsearch.md) of the Semantic Conventions for Elasticsearch client operations. The new attributes can be opted in to by setting the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a transition period for users to @@ -30,7 +30,7 @@ conventions are marked stable. At which time this instrumentation can receive a stable release, and the old database semantic conventions will no longer be supported. Refer to the - [specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md) + [specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/db/elasticsearch.md) for more information. When the new attributes are emitted: diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs index 1d9cd7c168..357f20e6e4 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/Implementation/ElasticsearchRequestPipelineDiagnosticListener.cs @@ -34,7 +34,7 @@ internal partial class ElasticsearchRequestPipelineDiagnosticListener : Listener private const string RequestRegexPattern = @"\n# Request:\r?\n(\{.*)\n# Response"; - private static readonly Version SemanticConventionsVersionNew = new(1, 42, 0); + private static readonly Version SemanticConventionsVersionNew = new(1, 43, 0); private static readonly ActivitySource ActivitySourceNew = ActivitySourceFactory.Create(SemanticConventionsVersionNew); private static readonly ActivitySource ActivitySourceBoth = ActivitySourceFactory.Create(null); diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md index 8e73ef1fa3..840a516d70 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/README.md @@ -18,9 +18,9 @@ and collects traces about outgoing requests. > [!NOTE] > This component is based on the OpenTelemetry semantic conventions for -[metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md#metrics) +[metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/db/elasticsearch.md#metrics) and -[traces](https://github.com/open-telemetry/semantic-conventions/blob/v1.42.0/docs/db/elasticsearch.md#spans). +[traces](https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/db/elasticsearch.md#spans). These conventions are [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), and hence, this package is a From 1caa3b647c8079135a20f2bce6b07fbd8848c2d5 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 9 Jul 2026 19:59:01 +0100 Subject: [PATCH 6/6] [ElasticsearchClient] Update CHANGELOG Move entry after merge with main. --- .../CHANGELOG.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md index b8a6e4f0bc..a0eb85fceb 100644 --- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md @@ -2,19 +2,6 @@ ## Unreleased -## 1.16.0-beta.1 - -Released 2026-Jul-09 - -* Update `System.Text.Json` for `netstandard2.0` and `net8.0` to `8.0.5`. - ([#4154](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4154)) - -* Updated OpenTelemetry core component version(s) to `1.16.0`. - ([#4487](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4487)) - -* Assemblies are now digitally signed using cosign. - ([#4637](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4637)) - * Updated the database semantic conventions to [v1.43.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.43.0/docs/db/elasticsearch.md) of the Semantic Conventions for Elasticsearch client operations. The new @@ -51,6 +38,19 @@ Released 2026-Jul-09 ([#4635](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4635)) +## 1.16.0-beta.1 + +Released 2026-Jul-09 + +* Update `System.Text.Json` for `netstandard2.0` and `net8.0` to `8.0.5`. + ([#4154](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4154)) + +* Updated OpenTelemetry core component version(s) to `1.16.0`. + ([#4487](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4487)) + +* Assemblies are now digitally signed using cosign. + ([#4637](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4637)) + ## 1.15.1-beta.1 Released 2026-Apr-21