Skip to content

Commit 996b615

Browse files
committed
Improve OTel bridge mapping of newer semcon for databases
1 parent 9fe2800 commit 996b615

4 files changed

Lines changed: 472 additions & 7 deletions

File tree

src/Elastic.Apm/OpenTelemetry/OTelActivityMapper.cs

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#if NET || NETSTANDARD2_1
77
using System;
8+
using System.Collections.Generic;
89
using System.Diagnostics;
910
using System.Linq;
1011
using Elastic.Apm.Api;
@@ -19,15 +20,38 @@ namespace Elastic.Apm.OpenTelemetry
1920
internal static class OTelActivityMapper
2021
{
2122
internal static readonly string[] ServerPortAttributeKeys =
22-
[SemanticConventions.ServerPort, SemanticConventions.NetPeerPort];
23+
[SemanticConventions.ServerPort, SemanticConventions.NetworkPeerPort, SemanticConventions.NetPeerPort];
2324

2425
internal static readonly string[] ServerAddressAttributeKeys =
25-
[SemanticConventions.ServerAddress, SemanticConventions.NetPeerName, SemanticConventions.NetPeerIp];
26+
[SemanticConventions.ServerAddress, SemanticConventions.NetworkPeerAddress, SemanticConventions.NetPeerName, SemanticConventions.NetPeerIp];
2627

2728
// Canonical URL/host-presence keys used both as "is this HTTP?" and for URL parsing.
2829
internal static readonly string[] HttpAttributeKeys =
2930
[SemanticConventions.UrlFull, SemanticConventions.HttpUrl];
3031

32+
internal static readonly string[] DbSystemAttributeKeys =
33+
[SemanticConventions.DbSystemName, SemanticConventions.DbSystem];
34+
35+
internal static readonly string[] DbInstanceAttributeKeys =
36+
[SemanticConventions.DbNamespace, SemanticConventions.DbName];
37+
38+
internal static readonly string[] DbQueryTextAttributeKeys =
39+
[SemanticConventions.DbQueryText, SemanticConventions.DbStatement];
40+
41+
// Systems where db.namespace is the confirmed stable replacement for db.name and maps correctly
42+
// to span.db.instance / service.target.name. Validated against each system's OTel spec page:
43+
// mongodb — db.namespace = database name
44+
// mysql — db.namespace = database name
45+
// cassandra — db.namespace = keyspace (the Cassandra equivalent of a database)
46+
// cosmosdb — db.namespace = database name ("azure.cosmosdb" is normalized to "cosmosdb" before this check)
47+
// Excluded systems and why:
48+
// elasticsearch — db.namespace = cluster name, not the index/alias being accessed
49+
// postgresql — db.namespace = "{database}|{schema}", composite; changes service target grouping vs db.name
50+
// mssql — db.namespace = "{instance}|{database}", composite; same concern as postgresql
51+
// All other systems use db.name only.
52+
internal static readonly HashSet<string> DbNamespaceAsInstanceSystems =
53+
new(StringComparer.OrdinalIgnoreCase) { "mongodb", "mysql", "cassandra", "cosmosdb" };
54+
3155
internal static void UpdateOTelAttributes(Activity activity, OTel otel)
3256
{
3357
var i = 0;
@@ -65,14 +89,15 @@ internal static void InferTransactionType(Transaction transaction, Activity acti
6589
internal static void InferSpanTypeAndSubType(Span span, Activity activity)
6690
{
6791
var peerPort = string.Empty;
68-
var netName = string.Empty;
92+
var peerAddress = string.Empty;
6993

7094
if (TryGetStringValue(activity, ServerPortAttributeKeys, out var netPortValue))
7195
peerPort = netPortValue;
7296

7397
if (TryGetStringValue(activity, ServerAddressAttributeKeys, out var netNameValue))
74-
netName = netNameValue;
98+
peerAddress = netNameValue;
7599

100+
var netName = peerAddress;
76101
if (netName.Length > 0 && peerPort.Length > 0)
77102
{
78103
netName += ':';
@@ -83,13 +108,37 @@ internal static void InferSpanTypeAndSubType(Span span, Activity activity)
83108
string serviceTargetName = null;
84109
string resource = null;
85110

86-
if (TryGetStringValue(activity, SemanticConventions.DbSystem, out var dbSystem))
111+
// db.system.name is the current OTel convention; db.system is the older one still used by many libraries.
112+
if (TryGetStringValue(activity, DbSystemAttributeKeys, out var dbSystem))
87113
{
114+
// Normalize OTel system names to the Elastic APM subtype values expected by APM server / Kibana.
115+
// The raw OTel value is preserved in otel.attributes; this only affects ECS-mapped fields.
116+
dbSystem = NormalizeDbSystem(dbSystem);
88117
span.Type = ApiConstants.TypeDb;
89118
span.Subtype = dbSystem;
119+
span.Action = ApiConstants.ActionQuery;
90120
serviceTargetType = span.Subtype;
91-
serviceTargetName = TryGetStringValue(activity, SemanticConventions.DbName, out var dbName) ? dbName : null;
121+
122+
// db.namespace semantics vary by system — only use it for systems where it is a direct
123+
// equivalent of the database name / instance concept (see DbNamespaceAsInstanceSystems).
124+
// For others (e.g. elasticsearch = cluster name, postgresql = "{db}|{schema}") fall back
125+
// to db.name only, to avoid incorrect service target grouping.
126+
string dbInstance = null;
127+
if (DbNamespaceAsInstanceSystems.Contains(dbSystem))
128+
TryGetStringValue(activity, DbInstanceAttributeKeys, out dbInstance);
129+
else
130+
TryGetStringValue(activity, SemanticConventions.DbName, out dbInstance);
131+
serviceTargetName = dbInstance;
92132
resource = ToResourceName(span.Subtype, serviceTargetName);
133+
134+
// db.query.text is the current OTel convention; db.statement is the older one.
135+
TryGetStringValue(activity, DbQueryTextAttributeKeys, out var dbStatement);
136+
span.Context.Db = new Database
137+
{
138+
Type = dbSystem,
139+
Instance = dbInstance,
140+
Statement = dbStatement
141+
};
93142
}
94143
else if (TryGetStringValue(activity, SemanticConventions.MessagingSystem, out var messagingSystem))
95144
{
@@ -154,6 +203,13 @@ internal static void InferSpanTypeAndSubType(Span span, Activity activity)
154203
span.Context.Destination ??= new Destination();
155204
span.Context.Destination.Service = new Destination.DestinationService { Resource = resource };
156205
}
206+
if (peerAddress.Length > 0)
207+
{
208+
span.Context.Destination ??= new Destination();
209+
span.Context.Destination.Address = peerAddress;
210+
if (peerPort.Length > 0 && int.TryParse(peerPort, out var parsedPort))
211+
span.Context.Destination.Port = parsedPort;
212+
}
157213
}
158214

159215
/// <summary>
@@ -237,6 +293,16 @@ private static string ParseNetName(string url)
237293
}
238294
}
239295

296+
/// <summary>
297+
/// Maps OTel <c>db.system.name</c> values to the Elastic APM subtype string expected by APM server.
298+
/// Only applied to ECS-mapped fields; raw OTel attributes are stored as-is.
299+
/// </summary>
300+
private static string NormalizeDbSystem(string dbSystem) =>
301+
// "azure.cosmosdb" is the stable OTel value; APM server / Kibana expect "cosmosdb" (matching the native integration).
302+
string.Equals(dbSystem, "azure.cosmosdb", StringComparison.OrdinalIgnoreCase)
303+
? ApiConstants.SubTypeCosmosDb
304+
: dbSystem;
305+
240306
private static string ToResourceName(string type, string name) =>
241307
string.IsNullOrEmpty(name) ? type : $"{type}/{name}";
242308
}

src/Elastic.Apm/OpenTelemetry/SemanticConventions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ internal static class SemanticConventions
44
{
55
// DATABASE
66
public const string DbSystem = "db.system";
7+
public const string DbSystemName = "db.system.name"; // newer OTel convention (replaces db.system)
78
public const string DbName = "db.name";
9+
// db.namespace is the current OTel convention replacing db.name. The value is system-specific:
10+
// MySQL/MongoDB: the database name. PostgreSQL: "{database}|{schema}". SQL Server: "{instance}|{database}". Redis: the numeric db index.
11+
public const string DbNamespace = "db.namespace";
12+
public const string DbStatement = "db.statement";
13+
public const string DbQueryText = "db.query.text"; // newer OTel convention (replaces db.statement)
814

915
// HTTP
1016
public const string HttpUrl = "http.url";
@@ -28,7 +34,11 @@ internal static class SemanticConventions
2834
public const string ServerAddress = "server.address";
2935
public const string ServerPort = "server.port";
3036

31-
// NET
37+
// NETWORK (stable, current)
38+
public const string NetworkPeerAddress = "network.peer.address";
39+
public const string NetworkPeerPort = "network.peer.port";
40+
41+
// NET (legacy)
3242
public const string NetPeerIp = "net.peer.ip";
3343
public const string NetPeerName = "net.peer.name";
3444
public const string NetPeerPort = "net.peer.port";

0 commit comments

Comments
 (0)