Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/OpenTelemetry.Api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Notes](../../RELEASENOTES.md).

## Unreleased

* Fixed `TracerProvider.GetTracer` caching a new tracer for each call with equal
array-valued scope tags.
([#7960](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7960))

## 1.20.0

Released 2026-Oct-15
Expand Down
91 changes: 79 additions & 12 deletions src/OpenTelemetry.Api/Trace/TracerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public Tracer GetTracer(
var activitySourceOptions = new System.Diagnostics.ActivitySourceOptions(key.Name)
{
Version = key.Version,
Tags = key.Tags,
Tags = key.GetActivitySourceTags(),
TelemetrySchemaUrl = key.SchemaUrl,
};

Expand Down Expand Up @@ -182,6 +182,10 @@ public override int GetHashCode()
}
}

// ActivitySource exposes its tags, so its arrays must not be shared with the cache key.
internal KeyValuePair<string, object?>[]? GetActivitySourceTags()
=> GetOrderedTags(this.Tags);

private static bool AreTagsEqual(
KeyValuePair<string, object?>[]? tags1,
KeyValuePair<string, object?>[]? tags2)
Expand All @@ -207,19 +211,44 @@ private static bool AreTagsEqual(
}

// Compare values
if (kvp1.Value is null)
if (!AreValuesEqual(kvp1.Value, kvp2.Value))
{
if (kvp2.Value is not null)
{
return false;
}
return false;
}
}

return true;
}

private static bool AreValuesEqual(object? value1, object? value2)
{
if (value1 is not Array array1 || value2 is not Array array2)
{
return Equals(value1, value2);
}

if (array1.GetType() != array2.GetType() || array1.Length != array2.Length)
{
return false;
}

for (var dimension = 0; dimension < array1.Rank; dimension++)
{
if (array1.GetLength(dimension) != array2.GetLength(dimension)
|| array1.GetLowerBound(dimension) != array2.GetLowerBound(dimension))
{
return false;
}
else
}

var enumerator1 = array1.GetEnumerator();
var enumerator2 = array2.GetEnumerator();
while (enumerator1.MoveNext() && enumerator2.MoveNext())
{
// Compare scalar elements without traversing arbitrary object graphs.
if (!Equals(enumerator1.Current, enumerator2.Current))
{
if (!kvp1.Value.Equals(kvp2.Value))
{
return false;
}
return false;
}
}

Expand All @@ -246,14 +275,43 @@ private static int GetTagsHashCode(
#endif
if (kvp.Value != null)
{
hash = (hash * 31) + kvp.Value.GetHashCode();
hash = (hash * 31) + GetValueHashCode(kvp.Value);
}
}
}

return hash;
}

private static int GetValueHashCode(object value)
{
if (value is not Array array)
{
return value.GetHashCode();
}

#if NET
HashCode hash = default;
foreach (var item in array)
{
hash.Add(item);
}

return hash.ToHashCode();
#else
var hash = 0;
unchecked
{
foreach (var item in array)
{
hash = (hash * 31) + (item?.GetHashCode() ?? 0);
}
}

return hash;
#endif
}

private static KeyValuePair<string, object?>[]? GetOrderedTags(
IEnumerable<KeyValuePair<string, object?>>? tags)
{
Expand All @@ -263,6 +321,15 @@ private static int GetTagsHashCode(
}

var orderedTagList = new List<KeyValuePair<string, object?>>(tags);
for (var i = 0; i < orderedTagList.Count; i++)
{
// Snapshot array values so caller mutations cannot change a cached key.
if (orderedTagList[i].Value is Array array)
{
orderedTagList[i] = new(orderedTagList[i].Key, array.Clone());
}
}

orderedTagList.Sort((left, right) =>
{
// First compare by key
Expand Down
126 changes: 126 additions & 0 deletions test/OpenTelemetry.Api.Tests/Trace/TracerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,132 @@ public void GetTracer_WithSameTags_ReturnsSameInstance()
Assert.Same(tracer1, tracer2);
}

[Fact]
public void GetTracer_WithEqualArrayTags_ReturnsSameInstance()
{
using var tracerProvider = new TestTracerProvider();
Tracer? firstTracer = null;

for (var i = 0; i < 10; i++)
{
// Each lookup supplies newly allocated arrays with the same values.
#pragma warning disable CA1825 // The cache must also match distinct empty arrays.
#pragma warning disable CA1861 // Each lookup must supply fresh arrays to exercise value equality.
KeyValuePair<string, object?>[] tags =
[
new("strings", new string?[] { "eu", null, "us" }),
new("booleans", new[] { true, false }),
new("integers", new[] { 1, 2 }),
new("longs", new[] { 1L, 2L }),
new("doubles", new[] { 1.0, double.NaN }),
new("floats", new[] { 1.0f, 2.0f }),
new("bytes", new byte[] { 1, 2 }),
new("shorts", new short[] { 1, 2 }),
new("chars", new[] { 'a', 'b' }),
new("empty", new string[0]),
];
#pragma warning restore CA1861 // Each lookup must supply fresh arrays to exercise value equality.
#pragma warning restore CA1825 // The cache must also match distinct empty arrays.

var tracer = tracerProvider.GetTracer("test", "1.0.0", tags);
firstTracer ??= tracer;
Assert.Same(firstTracer, tracer);
}

Assert.Single(tracerProvider.Tracers!);
}

[Theory]
[InlineData(new[] { "eu", "us" }, new[] { "eu", "uk" })]
[InlineData(new[] { "eu", "us" }, new[] { "us", "eu" })]
[InlineData(new[] { "eu", "us" }, new[] { "eu" })]
[InlineData(new[] { "eu" }, "eu")]
[InlineData(new[] { "eu" }, null)]
[InlineData(new[] { 1, 2 }, new[] { 1L, 2L })]
[InlineData(new int[0], new long[0])]
public void GetTracer_WithDifferentArrayTags_ReturnsDifferentInstances(object? value1, object? value2)
{
using var tracerProvider = new TestTracerProvider();
var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("regions", value1)]);
var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("regions", value2)]);

Assert.NotSame(tracer1, tracer2);
}

[Fact]
public void GetTracer_WithArrayTags_SnapshotsCallerValues()
{
using var tracerProvider = new TestTracerProvider();
var value = new[] { "eu", "us" };
KeyValuePair<string, object?>[] tags = [new("regions", value)];
var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags);

value[1] = "uk";
var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags);

value[1] = "us";
var tracer3 = tracerProvider.GetTracer("test", "1.0.0", tags);

Assert.NotSame(tracer1, tracer2);
Assert.Same(tracer1, tracer3);
Assert.Equal(2, tracerProvider.Tracers!.Count);

var sourceValue = Assert.IsType<string[]>(Assert.Single(tracer1.ActivitySource!.Tags!).Value);
Assert.NotSame(value, sourceValue);
Assert.Equal("eu", sourceValue[0]);
Assert.Equal("us", sourceValue[1]);
}

[Fact]
public void GetTracer_WithArrayTags_SourceMutationDoesNotChangeCacheKey()
{
using var tracerProvider = new TestTracerProvider();
var value = new[] { "eu", "us" };
KeyValuePair<string, object?>[] tags = [new("regions", value)];
var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags);

var sourceValue = Assert.IsType<string[]>(Assert.Single(tracer1.ActivitySource!.Tags!).Value);
sourceValue[1] = "uk";

var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags);

Assert.Same(tracer1, tracer2);
Assert.Single(tracerProvider.Tracers!);
Assert.Equal("us", value[1]);
}

[Fact]
public void GetTracer_WithMultidimensionalArrayTags_PreservesShape()
{
using var tracerProvider = new TestTracerProvider();
var value = Array.CreateInstance(typeof(int), 1, 2);
value.SetValue(1, 0, 0);
value.SetValue(2, 0, 1);
var differentShape = Array.CreateInstance(typeof(int), 2, 1);
differentShape.SetValue(1, 0, 0);
differentShape.SetValue(2, 1, 0);
var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("values", value)]);
var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("values", value.Clone())]);
var tracer3 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("values", differentShape)]);

Assert.Same(tracer1, tracer2);
Assert.NotSame(tracer1, tracer3);
}

[Fact]
public void GetTracer_WithSelfReferentialArrayTag_DoesNotRecurse()
{
using var tracerProvider = new TestTracerProvider();
var value = new object[1];
value[0] = value;

var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("values", value)]);
var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags: [new("values", value)]);

Assert.Same(tracer1, tracer2);
Assert.Single(tracerProvider.Tracers!);
}

[Fact]
public void GetTracer_WithoutTags_ReturnsSameInstance()
{
Expand Down
Loading