55
66#if NET || NETSTANDARD2_1
77using System ;
8+ using System . Collections . Generic ;
89using System . Diagnostics ;
910using System . Linq ;
1011using 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 }
0 commit comments