diff --git a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md
index e97e1d38b8..a0eb85fceb 100644
--- a/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md
+++ b/src/OpenTelemetry.Instrumentation.ElasticsearchClient/CHANGELOG.md
@@ -2,6 +2,42 @@
## Unreleased
+* 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
+ 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.43.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.
+ * `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))
+
## 1.16.0-beta.1
Released 2026-Jul-09
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..357f20e6e4 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,15 +28,58 @@ 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, 43, 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
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");
@@ -68,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}";
}
@@ -127,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();
@@ -195,7 +311,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);
@@ -206,32 +330,86 @@ private void OnStartActivity(Activity activity, object? payload)
}
var elasticIndex = GetElasticIndex(uri);
- activity.DisplayName = GetDisplayName(activity, method, elasticIndex);
- activity.SetTag(SemanticConventions.AttributeDbSystem, DatabaseSystemName);
+ var dbOperationName = emitNewAttributes ? GetDbOperationName(uri, method) : null;
+ activity.DisplayName = GetDisplayName(activity, method, elasticIndex, dbOperationName, emitNewAttributes && !emitOldAttributes);
+
+ if (emitOldAttributes)
+ {
+ activity.SetTag(SemanticConventions.AttributeDbSystem, DatabaseSystemName);
+ }
+
+ if (emitNewAttributes)
+ {
+ activity.SetTag(SemanticConventions.AttributeDbSystemName, DatabaseSystemName);
+
+ if (dbOperationName != null)
+ {
+ activity.SetTag(SemanticConventions.AttributeDbOperationName, dbOperationName);
+ }
+ }
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());
+ var methodName = method.ToString();
+
+ if (emitOldAttributes)
+ {
+ activity.SetTag(AttributeDbMethod, methodName);
+ }
+
+ if (emitNewAttributes)
+ {
+ activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, methodName);
+ }
}
activity.SetTag(SemanticConventions.AttributeUrlFull, uri.OriginalString);
@@ -251,12 +429,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 +458,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 +474,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 +506,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..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/main/docs/db/elasticsearch.md)
+[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/main/docs/db/elasticsearch.md).
+[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
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..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,12 +814,245 @@ 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);
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);
+ }
+
+ 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));
+
+ // 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);
+ }
+
+ var searchActivity = Assert.Single(exportedItems);
+
+ // 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("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]
+ 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);
+ }
+
+ var searchActivity = Assert.Single(exportedItems);
+
+ 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);
+ }
+
+ var searchActivity = Assert.Single(exportedItems);
+
+ 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);
+
+ 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);
+ }
}