Skip to content

Commit f2e383d

Browse files
committed
Add test infrastructure for OpenTelemetry
1 parent d763e6a commit f2e383d

File tree

8 files changed

+574
-6
lines changed

8 files changed

+574
-6
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/* Copyright 2025-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.Collections.Generic;
18+
using System.Diagnostics;
19+
20+
namespace MongoDB.Driver.TestHelpers.Core
21+
{
22+
public class CapturedSpan
23+
{
24+
public string Name { get; set; }
25+
public Dictionary<string, object> Attributes { get; set; }
26+
public ActivityStatusCode StatusCode { get; set; }
27+
public string StatusDescription { get; set; }
28+
public List<CapturedSpan> NestedSpans { get; set; }
29+
public string ParentId { get; set; }
30+
public string SpanId { get; set; }
31+
32+
public CapturedSpan()
33+
{
34+
Attributes = new Dictionary<string, object>();
35+
NestedSpans = new List<CapturedSpan>();
36+
}
37+
}
38+
39+
public class SpanCapturer : IDisposable
40+
{
41+
private readonly object _lock = new object();
42+
private readonly List<Activity> _completedActivities;
43+
private readonly ActivityListener _listener;
44+
45+
public SpanCapturer()
46+
{
47+
_completedActivities = new List<Activity>();
48+
49+
_listener = new ActivityListener
50+
{
51+
ShouldListenTo = source => source.Name == MongoTelemetry.ActivitySource.Name,
52+
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
53+
ActivityStopped = OnActivityStopped
54+
};
55+
56+
ActivitySource.AddActivityListener(_listener);
57+
}
58+
59+
public List<CapturedSpan> Spans
60+
{
61+
get
62+
{
63+
lock (_lock)
64+
{
65+
return BuildSpanTree(_completedActivities);
66+
}
67+
}
68+
}
69+
70+
public void Clear()
71+
{
72+
lock (_lock)
73+
{
74+
_completedActivities.Clear();
75+
}
76+
}
77+
78+
public void Dispose()
79+
{
80+
_listener?.Dispose();
81+
}
82+
83+
private void OnActivityStopped(Activity activity)
84+
{
85+
if (activity == null)
86+
{
87+
return;
88+
}
89+
90+
// Filter out hello/isMaster handshake commands for now
91+
// TODO: Discuss with spec owners whether handshake commands should be filtered
92+
var commandName = activity.GetTagItem("db.command.name") as string;
93+
if (commandName?.ToLowerInvariant() == "hello" || commandName?.ToLowerInvariant() == "ismaster")
94+
{
95+
return;
96+
}
97+
98+
lock (_lock)
99+
{
100+
_completedActivities.Add(activity);
101+
}
102+
}
103+
104+
private List<CapturedSpan> BuildSpanTree(List<Activity> activities)
105+
{
106+
var spanMap = new Dictionary<string, CapturedSpan>();
107+
108+
// Convert all activities to CapturedSpan
109+
foreach (var activity in activities)
110+
{
111+
var capturedSpan = new CapturedSpan
112+
{
113+
Name = activity.DisplayName ?? activity.OperationName,
114+
SpanId = activity.SpanId.ToString(),
115+
ParentId = activity.ParentSpanId.ToString(),
116+
StatusCode = activity.Status,
117+
StatusDescription = activity.StatusDescription
118+
};
119+
120+
// Capture all tags as attributes, preserving original types
121+
foreach (var tag in activity.TagObjects)
122+
{
123+
capturedSpan.Attributes[tag.Key] = tag.Value;
124+
}
125+
126+
spanMap[capturedSpan.SpanId] = capturedSpan;
127+
}
128+
129+
// Build parent-child relationships
130+
var rootSpans = new List<CapturedSpan>();
131+
foreach (var span in spanMap.Values)
132+
{
133+
if (span.ParentId == "00000000000000000000000000000000" || !spanMap.ContainsKey(span.ParentId))
134+
{
135+
// No parent or parent not in our captured set = root span
136+
rootSpans.Add(span);
137+
}
138+
else
139+
{
140+
// Add as child to parent
141+
spanMap[span.ParentId].NestedSpans.Add(span);
142+
}
143+
}
144+
145+
return rootSpans;
146+
}
147+
}
148+
}

tests/MongoDB.Driver.Tests/Specifications/UnifiedTestSpecRunner.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ public void LoadBalancers(JsonDrivenTestCase testCase)
157157
Run(testCase);
158158
}
159159

160+
[UnifiedTestsTheory("open_telemetry.operation")]
161+
public void OpenTelemetry(JsonDrivenTestCase testCase) => Run(testCase);
162+
163+
[UnifiedTestsTheory("open_telemetry.transaction")]
164+
public void OpenTelemetryTransactions(JsonDrivenTestCase testCase) => Run(testCase);
165+
160166
[Category("SupportLoadBalancing")]
161167
[UnifiedTestsTheory("read_write_concern.tests.operation")]
162168
public void ReadWriteConcern(JsonDrivenTestCase testCase) => Run(testCase);
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/* Copyright 2025-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.Collections.Generic;
18+
using FluentAssertions;
19+
using MongoDB.Bson;
20+
using MongoDB.Bson.IO;
21+
using MongoDB.Driver.TestHelpers.Core;
22+
using MongoDB.TestHelpers.XunitExtensions;
23+
using Xunit.Sdk;
24+
25+
namespace MongoDB.Driver.Tests.UnifiedTestOperations.Matchers
26+
{
27+
public class UnifiedSpanMatcher
28+
{
29+
private readonly UnifiedValueMatcher _valueMatcher;
30+
31+
public UnifiedSpanMatcher(UnifiedValueMatcher valueMatcher)
32+
{
33+
_valueMatcher = valueMatcher;
34+
}
35+
36+
public void AssertSpansMatch(List<CapturedSpan> actualSpans, BsonArray expectedSpans, bool ignoreExtraSpans)
37+
{
38+
try
39+
{
40+
AssertSpans(actualSpans, expectedSpans, ignoreExtraSpans);
41+
}
42+
catch (XunitException exception)
43+
{
44+
throw new AssertionException(
45+
userMessage: GetAssertionErrorMessage(actualSpans, expectedSpans),
46+
innerException: exception);
47+
}
48+
}
49+
50+
private void AssertSpans(List<CapturedSpan> actualSpans, BsonArray expectedSpans, bool ignoreExtraSpans)
51+
{
52+
if (ignoreExtraSpans)
53+
{
54+
actualSpans.Count.Should().BeGreaterOrEqualTo(expectedSpans.Count);
55+
56+
// When ignoring extra spans, find each expected span in order within the actual spans
57+
int actualIndex = 0;
58+
for (int expectedIndex = 0; expectedIndex < expectedSpans.Count; expectedIndex++)
59+
{
60+
var expectedSpan = expectedSpans[expectedIndex].AsBsonDocument;
61+
var expectedName = expectedSpan["name"].AsString;
62+
63+
// Find the next actual span that matches this expected span's name
64+
bool found = false;
65+
while (actualIndex < actualSpans.Count)
66+
{
67+
var actualSpan = actualSpans[actualIndex];
68+
if (actualSpan.Name == expectedName)
69+
{
70+
AssertSpan(actualSpan, expectedSpan);
71+
actualIndex++;
72+
found = true;
73+
break;
74+
}
75+
actualIndex++;
76+
}
77+
78+
if (!found)
79+
{
80+
throw new AssertionException($"Expected span with name '{expectedName}' not found in actual spans starting from index {actualIndex}");
81+
}
82+
}
83+
}
84+
else
85+
{
86+
actualSpans.Should().HaveSameCount(expectedSpans);
87+
88+
for (int i = 0; i < expectedSpans.Count; i++)
89+
{
90+
var actualSpan = actualSpans[i];
91+
var expectedSpan = expectedSpans[i].AsBsonDocument;
92+
93+
AssertSpan(actualSpan, expectedSpan);
94+
}
95+
}
96+
}
97+
98+
private void AssertSpan(CapturedSpan actualSpan, BsonDocument expectedSpan)
99+
{
100+
foreach (var element in expectedSpan)
101+
{
102+
switch (element.Name)
103+
{
104+
case "name":
105+
actualSpan.Name.Should().Be(element.Value.AsString);
106+
break;
107+
case "attributes":
108+
AssertAttributes(actualSpan.Attributes, element.Value.AsBsonDocument);
109+
break;
110+
case "nested":
111+
AssertNestedSpans(actualSpan.NestedSpans, element.Value.AsBsonArray);
112+
break;
113+
default:
114+
throw new FormatException($"Unexpected span field: '{element.Name}'.");
115+
}
116+
}
117+
}
118+
119+
private void AssertAttributes(Dictionary<string, object> actualAttributes, BsonDocument expectedAttributes)
120+
{
121+
foreach (var expectedAttribute in expectedAttributes)
122+
{
123+
var attributeName = expectedAttribute.Name;
124+
var expectedValue = expectedAttribute.Value;
125+
126+
// Check if this is a $$exists matcher
127+
if (expectedValue.IsBsonDocument)
128+
{
129+
var expectedDoc = expectedValue.AsBsonDocument;
130+
if (expectedDoc.Contains("$$exists"))
131+
{
132+
var shouldExist = expectedDoc["$$exists"].AsBoolean;
133+
if (shouldExist)
134+
{
135+
actualAttributes.Should().ContainKey(attributeName,
136+
$"span should have attribute '{attributeName}'");
137+
}
138+
else
139+
{
140+
actualAttributes.Should().NotContainKey(attributeName,
141+
$"span should not have attribute '{attributeName}'");
142+
}
143+
continue;
144+
}
145+
}
146+
147+
actualAttributes.Should().ContainKey(attributeName, $"span should have attribute '{attributeName}'");
148+
var actualValue = actualAttributes[attributeName];
149+
150+
// Convert the actual value to BsonValue
151+
var actualBsonValue = ConvertToBsonValue(actualValue);
152+
_valueMatcher.AssertValuesMatch(actualBsonValue, expectedValue);
153+
}
154+
}
155+
156+
private BsonValue ConvertToBsonValue(object value)
157+
{
158+
return value switch
159+
{
160+
null => BsonNull.Value,
161+
BsonValue bv => bv, // Already a BsonValue (including BsonDocument), return as-is
162+
string s => new BsonString(s),
163+
int i => new BsonInt32(i),
164+
long l => new BsonInt64(l),
165+
double d => new BsonDouble(d),
166+
bool b => new BsonBoolean(b),
167+
_ => throw new InvalidOperationException($"Unsupported span attribute type: {value.GetType().Name}")
168+
};
169+
}
170+
171+
private void AssertNestedSpans(List<CapturedSpan> actualNestedSpans, BsonArray expectedNestedSpans)
172+
{
173+
actualNestedSpans.Should().HaveSameCount(expectedNestedSpans, "nested spans count should match");
174+
175+
for (int i = 0; i < expectedNestedSpans.Count; i++)
176+
{
177+
AssertSpan(actualNestedSpans[i], expectedNestedSpans[i].AsBsonDocument);
178+
}
179+
}
180+
181+
private string GetAssertionErrorMessage(List<CapturedSpan> actualSpans, BsonArray expectedSpans)
182+
{
183+
var jsonWriterSettings = new JsonWriterSettings { Indent = true };
184+
185+
var actualSpansDocuments = new BsonArray();
186+
foreach (var actualSpan in actualSpans)
187+
{
188+
actualSpansDocuments.Add(ConvertSpanToBsonDocument(actualSpan));
189+
}
190+
191+
return
192+
$"Expected spans to be: {expectedSpans.ToJson(jsonWriterSettings)}{Environment.NewLine}" +
193+
$"But found: {actualSpansDocuments.ToJson(jsonWriterSettings)}.";
194+
}
195+
196+
private BsonDocument ConvertSpanToBsonDocument(CapturedSpan span)
197+
{
198+
var spanDocument = new BsonDocument
199+
{
200+
{ "name", span.Name },
201+
{ "status", span.StatusCode.ToString() }
202+
};
203+
204+
if (span.Attributes.Count > 0)
205+
{
206+
var attributesDocument = new BsonDocument();
207+
foreach (var attribute in span.Attributes)
208+
{
209+
attributesDocument[attribute.Key] = ConvertToBsonValue(attribute.Value);
210+
}
211+
spanDocument["attributes"] = attributesDocument;
212+
}
213+
214+
if (span.NestedSpans.Count > 0)
215+
{
216+
var nestedArray = new BsonArray();
217+
foreach (var nestedSpan in span.NestedSpans)
218+
{
219+
nestedArray.Add(ConvertSpanToBsonDocument(nestedSpan));
220+
}
221+
spanDocument["nested"] = nestedArray;
222+
}
223+
224+
return spanDocument;
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)