Skip to content

Commit e863c98

Browse files
committed
Add core OpenTelemetry infrastructure
1 parent 119ebac commit e863c98

File tree

3 files changed

+312
-0
lines changed

3 files changed

+312
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using MongoDB.Shared;
17+
18+
namespace MongoDB.Driver.Core.Configuration;
19+
20+
/// <summary>
21+
/// Tracing-related settings for MongoDB operations.
22+
/// </summary>
23+
public sealed class TracingOptions
24+
{
25+
/// <summary>
26+
/// Gets or sets whether tracing is disabled for this client.
27+
/// When set to true, no OpenTelemetry activities will be created for this client's operations.
28+
/// Default is false (tracing enabled if configured via TracerProvider).
29+
/// </summary>
30+
public bool Disabled { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets the maximum length of the db.query.text attribute.
34+
/// Default is 0 (attribute not added).
35+
/// </summary>
36+
public int QueryTextMaxLength { get; set; }
37+
38+
internal TracingOptions Clone()
39+
{
40+
return new TracingOptions
41+
{
42+
QueryTextMaxLength = QueryTextMaxLength,
43+
Disabled = Disabled
44+
};
45+
}
46+
47+
/// <summary>
48+
/// Determines whether the specified TracingOptions is equal to this instance.
49+
/// </summary>
50+
public bool Equals(TracingOptions other)
51+
{
52+
if (ReferenceEquals(null, other)) return false;
53+
if (ReferenceEquals(this, other)) return true;
54+
55+
return QueryTextMaxLength == other.QueryTextMaxLength && Disabled == other.Disabled;
56+
}
57+
58+
/// <inheritdoc/>
59+
public override bool Equals(object obj)
60+
{
61+
return ReferenceEquals(this, obj) || obj is TracingOptions other && Equals(other);
62+
}
63+
64+
/// <inheritdoc/>
65+
public override int GetHashCode()
66+
{
67+
return new Hasher()
68+
.Hash(QueryTextMaxLength)
69+
.Hash(Disabled)
70+
.GetHashCode();
71+
}
72+
}

src/MongoDB.Driver/MongoDB.Driver.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<PackageReference Include="Snappier" Version="1.0.0" />
2727
<PackageReference Include="ZstdSharp.Port" Version="0.7.3" />
2828
<PackageReference Include="System.Buffers" Version="4.5.1" />
29+
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
2930
</ItemGroup>
3031

3132
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Diagnostics;
18+
using System.Net;
19+
using System.Net.Sockets;
20+
using MongoDB.Bson;
21+
using MongoDB.Driver.Core.Connections;
22+
23+
namespace MongoDB.Driver;
24+
25+
/// <summary>
26+
/// Provides access to MongoDB driver's OpenTelemetry instrumentation.
27+
/// </summary>
28+
public static class MongoTelemetry
29+
{
30+
private static readonly string s_driverVersion = ClientDocumentHelper.GetAssemblyVersion(typeof(MongoClient).Assembly);
31+
32+
/// <summary>
33+
/// The ActivitySource used by MongoDB driver for OpenTelemetry tracing.
34+
/// Applications can subscribe to this source to receive MongoDB traces.
35+
/// </summary>
36+
public static readonly ActivitySource ActivitySource = new("MongoDB.Driver", s_driverVersion);
37+
38+
internal static Activity StartOperationActivity(string operationName, string databaseName, string collectionName = null)
39+
{
40+
if (string.IsNullOrEmpty(operationName))
41+
{
42+
return null;
43+
}
44+
45+
var spanName = GetSpanName(operationName, databaseName, collectionName);
46+
var activity = ActivitySource.StartActivity(spanName, ActivityKind.Client);
47+
48+
if (activity?.IsAllDataRequested == true)
49+
{
50+
activity.SetTag("db.system", "mongodb");
51+
activity.SetTag("db.operation.name", operationName);
52+
activity.SetTag("db.operation.summary", spanName);
53+
54+
if (!string.IsNullOrEmpty(databaseName))
55+
{
56+
activity.SetTag("db.namespace", databaseName);
57+
}
58+
59+
if (!string.IsNullOrEmpty(collectionName))
60+
{
61+
activity.SetTag("db.collection.name", collectionName);
62+
}
63+
}
64+
65+
return activity;
66+
}
67+
68+
internal static Activity StartTransactionActivity()
69+
{
70+
var activity = ActivitySource.StartActivity("transaction", ActivityKind.Client);
71+
72+
if (activity?.IsAllDataRequested == true)
73+
{
74+
activity.SetTag("db.system", "mongodb");
75+
}
76+
77+
return activity;
78+
}
79+
80+
internal static string GetSpanName(string name, string databaseName, string collectionName)
81+
{
82+
if (!string.IsNullOrEmpty(collectionName))
83+
{
84+
return $"{name} {databaseName}.{collectionName}";
85+
}
86+
if (!string.IsNullOrEmpty(databaseName))
87+
{
88+
return $"{name} {databaseName}";
89+
}
90+
return name;
91+
}
92+
93+
internal static Activity StartCommandActivity(
94+
string commandName,
95+
BsonDocument command,
96+
DatabaseNamespace databaseNamespace,
97+
ConnectionId connectionId,
98+
int queryTextMaxLength = 0)
99+
{
100+
var activity = ActivitySource.StartActivity(commandName, ActivityKind.Client);
101+
102+
if (activity == null)
103+
{
104+
return null;
105+
}
106+
107+
if (activity.IsAllDataRequested)
108+
{
109+
var collectionName = ExtractCollectionName(command);
110+
activity.SetTag("db.system", "mongodb");
111+
activity.SetTag("db.command.name", commandName);
112+
activity.SetTag("db.namespace", databaseNamespace.DatabaseName);
113+
114+
if (!string.IsNullOrEmpty(collectionName))
115+
{
116+
activity.SetTag("db.collection.name", collectionName);
117+
}
118+
119+
// db.query.summary uses the full format like operation-level spans
120+
var querySummary = GetSpanName(commandName, databaseNamespace.DatabaseName, collectionName);
121+
activity.SetTag("db.query.summary", querySummary);
122+
123+
SetConnectionTags(activity, connectionId);
124+
125+
if (command != null)
126+
{
127+
if (command.TryGetValue("lsid", out var lsid))
128+
{
129+
// Materialize the lsid to avoid accessing disposed RawBsonDocument later
130+
var materializedLsid = lsid.IsBsonDocument
131+
? new BsonDocument(lsid.AsBsonDocument)
132+
: lsid;
133+
activity.SetTag("db.mongodb.lsid", materializedLsid);
134+
}
135+
136+
if (command.TryGetValue("txnNumber", out var txnNumber))
137+
{
138+
activity.SetTag("db.mongodb.txn_number", txnNumber.ToInt64());
139+
}
140+
141+
if (queryTextMaxLength > 0)
142+
{
143+
SetQueryText(activity, command, queryTextMaxLength);
144+
}
145+
}
146+
}
147+
148+
return activity;
149+
}
150+
151+
internal static void RecordException(Activity activity, Exception exception)
152+
{
153+
if (activity == null)
154+
{
155+
return;
156+
}
157+
158+
activity.SetTag("exception.type", exception.GetType().FullName);
159+
activity.SetTag("exception.message", exception.Message);
160+
if (exception.StackTrace != null)
161+
{
162+
activity.SetTag("exception.stacktrace", exception.StackTrace);
163+
}
164+
activity.SetStatus(ActivityStatusCode.Error);
165+
}
166+
167+
private static void SetConnectionTags(Activity activity, ConnectionId connectionId)
168+
{
169+
var endPoint = connectionId?.ServerId?.EndPoint;
170+
switch (endPoint)
171+
{
172+
case IPEndPoint ipEndPoint:
173+
activity.SetTag("server.address", ipEndPoint.Address.ToString());
174+
activity.SetTag("server.port", (long)ipEndPoint.Port);
175+
activity.SetTag("network.transport", "tcp");
176+
break;
177+
case DnsEndPoint dnsEndPoint:
178+
activity.SetTag("server.address", dnsEndPoint.Host);
179+
activity.SetTag("server.port", (long)dnsEndPoint.Port);
180+
activity.SetTag("network.transport", "tcp");
181+
break;
182+
#if NET5_0_OR_GREATER || NETCOREAPP3_0_OR_GREATER
183+
case UnixDomainSocketEndPoint unixEndPoint:
184+
activity.SetTag("network.transport", "unix");
185+
activity.SetTag("server.address", unixEndPoint.ToString());
186+
break;
187+
#endif
188+
}
189+
190+
if (connectionId != null)
191+
{
192+
if (connectionId.LongServerValue.HasValue)
193+
{
194+
activity.SetTag("db.mongodb.server_connection_id", connectionId.LongServerValue.Value);
195+
}
196+
activity.SetTag("db.mongodb.driver_connection_id", connectionId.LongLocalValue);
197+
}
198+
}
199+
200+
private static void SetQueryText(Activity activity, BsonDocument command, int maxLength)
201+
{
202+
var commandToLog = FilterSensitiveData(command);
203+
var commandText = commandToLog.ToJson();
204+
205+
if (commandText.Length > maxLength)
206+
{
207+
commandText = commandText.Substring(0, maxLength);
208+
}
209+
210+
activity?.SetTag("db.query.text", commandText);
211+
}
212+
213+
private static BsonDocument FilterSensitiveData(BsonDocument command)
214+
{
215+
var filtered = new BsonDocument(command);
216+
filtered.Remove("lsid");
217+
filtered.Remove("$db");
218+
filtered.Remove("$clusterTime");
219+
filtered.Remove("signature");
220+
return filtered;
221+
}
222+
223+
private static string ExtractCollectionName(BsonDocument command)
224+
{
225+
if (command == null) return null;
226+
227+
var firstElement = command.GetElement(0);
228+
if (firstElement.Value.IsString)
229+
{
230+
var value = firstElement.Value.AsString;
231+
if (value != "1" && value != "admin" && !string.IsNullOrEmpty(value))
232+
{
233+
return value;
234+
}
235+
}
236+
237+
return null;
238+
}
239+
}

0 commit comments

Comments
 (0)