Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ef01b16
Add support for excluding tag keys in MetricStreamConfiguration and r…
nabutabu Jun 8, 2026
d123bfb
Add support for excluding tag keys in Exemplar and related classes
nabutabu Jun 8, 2026
6a9f7f7
Add unit test for excluding tag keys in exemplars
nabutabu Jun 8, 2026
276c3e2
Remove commented explanations for include and exclude modes in ReadOn…
nabutabu Jun 8, 2026
b485e01
lint
nabutabu Jun 8, 2026
c74635c
Merge branch 'main' into exclude-tags-1
nabutabu Jun 9, 2026
d7edccf
Refactor ExemplarReservoirFactory property to improve clarity and mai…
nabutabu Jun 9, 2026
40d0a8f
Empty tag keys is a valid configuration
nabutabu Jun 9, 2026
3621029
Merge branch 'main' into exclude-tags-1
nabutabu Jun 16, 2026
28d39d7
Merge branch 'main' into exclude-tags-1
nabutabu Jun 16, 2026
b367657
Changelog updated
nabutabu Jun 16, 2026
c75dcca
lint
nabutabu Jun 16, 2026
06f0ba9
Update src/OpenTelemetry/Metrics/AggregatorStore.cs
nabutabu Jun 17, 2026
eb13d51
Rename ExcludedTagKeysInteresting to ExcludedTagKeys
nabutabu Jun 17, 2026
b2ff4fa
remove multiple access of tags[n]
nabutabu Jun 17, 2026
56ce766
Merge branch 'main' into exclude-tags-1
nabutabu Jun 17, 2026
2bb5367
Update src/OpenTelemetry/Metrics/View/MetricStreamConfiguration.cs
nabutabu Jun 19, 2026
7a05799
Clarify TagKeys order sensitivity behavior
nabutabu Jun 19, 2026
a29f584
clarify default behavior
nabutabu Jun 19, 2026
185edda
ExcludedTagKeys condition update and update MetricViewTests to use em…
nabutabu Jun 19, 2026
43fe3fa
Merge branch 'main' into exclude-tags-1
nabutabu Jun 19, 2026
99d7c4b
typo
nabutabu Jun 26, 2026
636f71b
changelog wording changed
nabutabu Jun 26, 2026
c2dffca
Merge branch 'main' into exclude-tags-1
nabutabu Jun 26, 2026
f69791c
Update MetricsViewBenchmarks for new benchmarks on exclude tag keys
nabutabu Jun 27, 2026
e4af536
Merge branch 'main' into exclude-tags-1
nabutabu Jul 3, 2026
6010cdd
direct feedback
Kielek Jul 13, 2026
f544cdd
Merge branch 'main' into exclude-tags-1
Kielek Jul 13, 2026
b7893ac
Merge branch 'main' into exclude-tags-1
nabutabu Jul 14, 2026
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
2 changes: 2 additions & 0 deletions src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OpenTelemetry.Metrics.MetricStreamConfiguration.ExcludedTagKeys.get -> string![]?
OpenTelemetry.Metrics.MetricStreamConfiguration.ExcludedTagKeys.set -> void
4 changes: 4 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Notes](../../RELEASENOTES.md).
* Fixed a metric point reclaim data race on CPU ARM architectures.
([#7401](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7401))

* Added `ExcludedTagKeys` property to `MetricStreamConfiguration` and exemplar
Comment thread
nabutabu marked this conversation as resolved.
Outdated
filtering for excluding tag keys from metrics.
([#7373](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7373))

## 1.16.0

Released 2026-Jun-10
Expand Down
60 changes: 54 additions & 6 deletions src/OpenTelemetry/Metrics/AggregatorStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ internal sealed class AggregatorStore
{
#if NET
internal readonly FrozenSet<string>? TagKeysInteresting;
internal readonly FrozenSet<string>? ExcludedTagKeysInteresting;
Comment thread
Kielek marked this conversation as resolved.
Outdated
#else
internal readonly HashSet<string>? TagKeysInteresting;
internal readonly HashSet<string>? ExcludedTagKeysInteresting;
#endif
internal readonly bool OutputDelta;
internal readonly bool IsAsynchronous;
Expand Down Expand Up @@ -81,12 +83,7 @@ internal AggregatorStore(
this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale;
this.StartTimeExclusive = DateTimeOffset.UtcNow;
this.ExemplarReservoirFactory = exemplarReservoirFactory;
if (metricStreamIdentity.TagKeys == null)
{
this.updateLongCallback = this.UpdateLong;
this.updateDoubleCallback = this.UpdateDouble;
}
else
if (metricStreamIdentity.TagKeys != null)
{
this.updateLongCallback = this.UpdateLongCustomTags;
this.updateDoubleCallback = this.UpdateDoubleCustomTags;
Expand All @@ -98,6 +95,22 @@ internal AggregatorStore(
this.TagKeysInteresting = hs;
this.tagsKeysInterestingCount = hs.Count;
}
else if (metricStreamIdentity.ExcludedTagKeys != null)
Comment thread
nabutabu marked this conversation as resolved.
Outdated
{
this.updateLongCallback = this.UpdateLongExcludeTags;
this.updateDoubleCallback = this.UpdateDoubleExcludeTags;
#if NET
var hs = FrozenSet.ToFrozenSet(metricStreamIdentity.ExcludedTagKeys, StringComparer.Ordinal);
#else
var hs = new HashSet<string>(metricStreamIdentity.ExcludedTagKeys, StringComparer.Ordinal);
#endif
this.ExcludedTagKeysInteresting = hs;
Comment thread
nabutabu marked this conversation as resolved.
Outdated
}
else
{
this.updateLongCallback = this.UpdateLong;
this.updateDoubleCallback = this.UpdateDouble;
}

this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter;
Debug.Assert(
Expand Down Expand Up @@ -991,6 +1004,20 @@ private void UpdateDoubleCustomTags(double value, ReadOnlySpan<KeyValuePair<stri
this.UpdateDoubleMetricPoint(index, value, tags);
}

private void UpdateLongExcludeTags(long value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
{
var index = this.FindMetricAggregatorsExcludeTag(tags);

this.UpdateLongMetricPoint(index, value, tags);
}

private void UpdateDoubleExcludeTags(double value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
{
var index = this.FindMetricAggregatorsExcludeTag(tags);

this.UpdateDoubleMetricPoint(index, value, tags);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateDoubleMetricPoint(int metricPointIndex, double value, ReadOnlySpan<KeyValuePair<string, object?>> tags)
{
Expand Down Expand Up @@ -1064,4 +1091,25 @@ private int FindMetricAggregatorsCustomTag(ReadOnlySpan<KeyValuePair<string, obj

return this.lookupAggregatorStore(tagKeysAndValues!, actualLength);
}

private int FindMetricAggregatorsExcludeTag(ReadOnlySpan<KeyValuePair<string, object?>> tags)
{
var tagLength = tags.Length;
if (tagLength == 0)
{
this.InitializeZeroTagPointIfNotInitialized();
return 0;
}

var storage = ThreadStaticStorage.GetStorage();
storage.SplitToKeysAndValuesExclude(tags, tagLength, this.ExcludedTagKeysInteresting!, out var tagKeysAndValues, out var actualLength);

if (actualLength == 0)
{
this.InitializeZeroTagPointIfNotInitialized();
return 0;
}

return this.lookupAggregatorStore(tagKeysAndValues!, actualLength);
}
}
12 changes: 8 additions & 4 deletions src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ public struct Exemplar
{
#if NET
internal FrozenSet<string>? ViewDefinedTagKeys;
internal FrozenSet<string>? ExcludedTagKeys;
#else
internal HashSet<string>? ViewDefinedTagKeys;
internal HashSet<string>? ExcludedTagKeys;
#endif

private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, [], count: 0);
private static readonly ReadOnlyFilteredTagCollection Empty = new(includeKeys: null, excludeKeys: null, [], count: 0);
private int tagCount;
private KeyValuePair<string, object?>[]? tagStorage;
private MetricPointValueStorage valueStorage;
Expand Down Expand Up @@ -69,10 +71,11 @@ public double DoubleValue
/// <remarks>
/// Note: <see cref="FilteredTags"/> represents the set of tags which were
/// supplied at measurement but dropped due to filtering configured by a
/// view (<see cref="MetricStreamConfiguration.TagKeys"/>). If view tag
/// view (<see cref="MetricStreamConfiguration.TagKeys"/> or
/// <see cref="MetricStreamConfiguration.ExcludedTagKeys"/>). If view tag
/// filtering is not configured <see cref="FilteredTags"/> will be empty.
/// </remarks>
public readonly ReadOnlyFilteredTagCollection FilteredTags => this.tagCount == 0 ? Empty : new(this.ViewDefinedTagKeys, this.tagStorage!, this.tagCount);
public readonly ReadOnlyFilteredTagCollection FilteredTags => this.tagCount == 0 ? Empty : new(this.ViewDefinedTagKeys, this.ExcludedTagKeys, this.tagStorage!, this.tagCount);

internal void Update<T>(in ExemplarMeasurement<T> measurement)
where T : struct
Expand Down Expand Up @@ -116,7 +119,7 @@ internal void Update<T>(in ExemplarMeasurement<T> measurement)
this.SpanId = default;
}

if (this.ViewDefinedTagKeys != null)
if (this.ViewDefinedTagKeys != null || this.ExcludedTagKeys != null)
{
this.StoreRawTags(measurement.Tags);
}
Expand Down Expand Up @@ -160,6 +163,7 @@ internal readonly void Copy(ref Exemplar destination)
destination.SpanId = this.SpanId;
destination.valueStorage = this.valueStorage;
destination.ViewDefinedTagKeys = this.ViewDefinedTagKeys;
destination.ExcludedTagKeys = this.ExcludedTagKeys;
destination.tagCount = this.tagCount;
if (destination.tagCount > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,14 @@ public sealed override ReadOnlyExemplarCollection Collect()
internal sealed override void Initialize(AggregatorStore aggregatorStore)
{
var viewDefinedTagKeys = aggregatorStore.TagKeysInteresting;
var excludedTagKeys = aggregatorStore.ExcludedTagKeysInteresting;

for (var i = 0; i < this.runningExemplars.Length; i++)
{
this.runningExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys;
this.runningExemplars[i].ExcludedTagKeys = excludedTagKeys;
this.snapshotExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys;
this.snapshotExemplars[i].ExcludedTagKeys = excludedTagKeys;
}

base.Initialize(aggregatorStore);
Expand Down
13 changes: 13 additions & 0 deletions src/OpenTelemetry/Metrics/MeterProviderSdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ internal static void MeasurementRecordedDouble(Instrument instrument, double val
OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration.");
}

if (metricStreamConfig != null)
{
try
{
metricStreamConfig.Validate();
}
catch (Exception ex)
{
OpenTelemetrySdkEventSource.Log.MetricViewIgnored(instrument.Name, instrument.Meter.Name, ex.Message, "Fix the view configuration.");
metricStreamConfig = null;
}
}

if (metricStreamConfig != null)
{
metricStreamConfigs.Add(metricStreamConfig);
Expand Down
18 changes: 14 additions & 4 deletions src/OpenTelemetry/Metrics/MetricStreamIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me
this.ViewId = metricStreamConfiguration?.ViewId;
this.MetricStreamName = $"{this.MeterName}.{this.MeterVersion}.{this.InstrumentName}";
this.TagKeys = metricStreamConfiguration?.CopiedTagKeys;
this.ExcludedTagKeys = metricStreamConfiguration?.CopiedExcludedTagKeys;
this.HistogramBucketBounds = GetExplicitBucketHistogramBounds(instrument, metricStreamConfiguration);
this.HistogramBucketDisplayBounds = GetExplicitBucketHistogramDisplayBounds(instrument, this.HistogramBucketBounds);
this.ExponentialHistogramMaxSize = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxSize ?? 0;
Expand All @@ -43,12 +44,13 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me
hashCode.Add(this.Description);
hashCode.Add(this.ViewId);

// Note: The this.TagKeys! here is strange but it is fine for the value
// to be null. HashCode.Add is coded to handle the value being null. We
// are essentially suppressing a false positive due to an issue/quirk
// with the annotations. See:
// Note: The this.TagKeys! / this.ExcludedTagKeys! here is strange but
// it is fine for the value to be null. HashCode.Add is coded to handle
// the value being null. We are essentially suppressing a false positive
// due to an issue/quirk with the annotations. See:
// https://github.com/dotnet/runtime/pull/91905.
hashCode.Add(this.TagKeys!, StringArrayComparer);
hashCode.Add(this.ExcludedTagKeys!, StringArrayComparer);

hashCode.Add(this.ExponentialHistogramMaxSize);
hashCode.Add(this.ExponentialHistogramMaxScale);
Expand Down Expand Up @@ -78,6 +80,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me
hash = (hash * 31) + this.Description.GetHashCode();
hash = (hash * 31) + (this.ViewId ?? 0);
hash = (hash * 31) + (this.TagKeys != null ? StringArrayComparer.GetHashCode(this.TagKeys) : 0);
hash = (hash * 31) + (this.ExcludedTagKeys != null ? StringArrayComparer.GetHashCode(this.ExcludedTagKeys) : 0);
if (this.HistogramBucketBounds != null)
{
var len = this.HistogramBucketBounds.Length;
Expand Down Expand Up @@ -114,6 +117,12 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me

public string[]? TagKeys { get; }

// Note: ExcludedTagKeys uses element-wise comparison (same as TagKeys).
Comment thread
nabutabu marked this conversation as resolved.
// ["a","b"] and ["b","a"] produce different stream identities even though
// the filtering behavior is identical. This is a pre-existing limitation
// inherited from TagKeys.
public string[]? ExcludedTagKeys { get; }

public double[]? HistogramBucketBounds { get; }

public double[]? HistogramBucketDisplayBounds { get; }
Expand Down Expand Up @@ -173,6 +182,7 @@ public bool Equals(MetricStreamIdentity other)
&& this.ExponentialHistogramMaxSize == other.ExponentialHistogramMaxSize
&& this.ExponentialHistogramMaxScale == other.ExponentialHistogramMaxScale
&& StringArrayComparer.Equals(this.TagKeys, other.TagKeys)
&& StringArrayComparer.Equals(this.ExcludedTagKeys, other.ExcludedTagKeys)
&& HistogramBoundsEqual(this.HistogramBucketBounds, other.HistogramBucketBounds);

public override readonly int GetHashCode() => this.hashCode;
Expand Down
60 changes: 60 additions & 0 deletions src/OpenTelemetry/Metrics/ThreadStaticStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,66 @@ internal void SplitToKeysAndValues(
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SplitToKeysAndValuesExclude(
ReadOnlySpan<KeyValuePair<string, object?>> tags,
int tagLength,
#if NET
FrozenSet<string> excludedTagKeys,
#else
HashSet<string> excludedTagKeys,
#endif
out KeyValuePair<string, object?>[]? tagKeysAndValues,
out int actualLength)
{
// Worst case: no tags excluded, all survive.
var maxLength = tagLength;
tagKeysAndValues = maxLength == 0
? null
: maxLength <= MaxTagCacheSize
? this.primaryTagStorage[maxLength - 1].TagKeysAndValues
: (new KeyValuePair<string, object?>[maxLength]);

actualLength = 0;
for (var n = 0; n < tagLength; n++)
{
// Copy only tags NOT in the excluded set.
if (!excludedTagKeys.Contains(tags[n].Key))
{
tagKeysAndValues![actualLength] = tags[n];
Comment thread
Kielek marked this conversation as resolved.
Outdated
actualLength++;
}
}

if (actualLength < maxLength)
{
if (actualLength == 0)
{
tagKeysAndValues = null;
return;
}

Debug.Assert(tagKeysAndValues != null, "tagKeysAndValues was null");

if (actualLength <= MaxTagCacheSize)
{
var tmpTagKeysAndValues = this.primaryTagStorage[actualLength - 1].TagKeysAndValues;

Array.Copy(tagKeysAndValues, 0, tmpTagKeysAndValues, 0, actualLength);

tagKeysAndValues = tmpTagKeysAndValues;
}
else
{
var tmpTagKeysAndValues = new KeyValuePair<string, object?>[actualLength];

Array.Copy(tagKeysAndValues, 0, tmpTagKeysAndValues, 0, actualLength);

tagKeysAndValues = tmpTagKeysAndValues;
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void CloneKeysAndValues(
KeyValuePair<string, object?>[] inputTagKeysAndValues,
Expand Down
39 changes: 39 additions & 0 deletions src/OpenTelemetry/Metrics/View/MetricStreamConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,32 @@ public int? CardinalityLimit
}
}

#pragma warning disable CA1819 // Properties should not return arrays
/// <summary>
/// Gets or sets the optional tag keys to exclude from the metric stream.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>If not provided, all the tags provided by the instrument
Comment thread
nabutabu marked this conversation as resolved.
/// while reporting measurements will be used for aggregation.
/// If provided, only those tags not in this list will be used
/// for aggregation. Providing an empty array will result in no tags
/// being excluded (same effect as default behavior
/// all attributes are preserved).
Comment thread
nabutabu marked this conversation as resolved.
Outdated
/// </item>
/// <item>A copy is made of the provided array.</item>
/// <item>This is mutually exclusive with <see cref="TagKeys"/>
/// setting both will cause the view to be ignored.</item>
/// </list>
/// </remarks>
Comment thread
nabutabu marked this conversation as resolved.
public string[]? ExcludedTagKeys
#pragma warning restore CA1819 // Properties should not return arrays
{
get => this.CopiedExcludedTagKeys?.ToArray();
set => this.CopiedExcludedTagKeys = value?.ToArray();
}

#if EXPOSE_EXPERIMENTAL_FEATURES
/// <summary>
/// Gets or sets a factory function used to generate an <see
Expand All @@ -117,5 +143,18 @@ public int? CardinalityLimit

internal string[]? CopiedTagKeys { get; private set; }

internal string[]? CopiedExcludedTagKeys { get; private set; }

internal int? ViewId { get; set; }

internal void Validate()
{
if (this.CopiedTagKeys != null && this.CopiedExcludedTagKeys != null)
{
throw new InvalidOperationException(
"TagKeys and ExcludedTagKeys cannot both be set. They are mutually exclusive. " +
"Use only one filtering strategy per view. For combined behavior, use the " +
"Func<Instrument, MetricStreamConfiguration> overload of AddView with custom logic.");
}
}
}
Loading