Skip to content
3 changes: 3 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Released 2025-Oct-01
}
```

* Moved the `InstrumentNameRegex` from `MeterProviderBuilderSdk` to `Guard`.
([#6457](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6457))

Comment on lines +32 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like an internal implementation detail - all users need to know is what the external-facing behaviour change is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I added it to the changelog because the regex is public and someone may change it using reflection:
#6457 (comment)

I can also omit this in the changelog, if it is not necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah OK - I didn't realise it was public.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't . It was known internal feature used by reflection. Mostly by MS teams.

* Fixed parsing of `OTEL_TRACES_SAMPLER_ARG` decimal values to always use `.`
as the delimiter when using the `traceidratio` sampler, preventing
locale-specific parsing issues.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProvid
{
Guard.ThrowIfNull(instrumentName);

if (!MeterProviderBuilderSdk.IsValidInstrumentName(name))
{
throw new ArgumentException($"Custom view name {name} is invalid.", nameof(name));
}
MetricGuard.ThrowIfInvalidCustomViewName(name);

#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1
#if NET || NETSTANDARD2_1_OR_GREATER
Expand Down
42 changes: 0 additions & 42 deletions src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Internal;
using OpenTelemetry.Resources;
Expand All @@ -27,14 +26,6 @@ public MeterProviderBuilderSdk(IServiceProvider serviceProvider)
this.serviceProvider = serviceProvider;
}

// Note: We don't use static readonly here because some customers
// replace this using reflection which is not allowed on initonly static
// fields. See: https://github.com/dotnet/runtime/issues/11571.
// Customers: This is not guaranteed to work forever. We may change this
// mechanism in the future do this at your own risk.
public static Regex InstrumentNameRegex { get; set; } = new(
@"^[a-z][a-z0-9-._/]{0,254}$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

public List<InstrumentationRegistration> Instrumentation { get; } = new();

public ResourceBuilder? ResourceBuilder { get; private set; }
Expand All @@ -53,39 +44,6 @@ public MeterProviderBuilderSdk(IServiceProvider serviceProvider)

public int CardinalityLimit { get; private set; } = DefaultCardinalityLimit;

/// <summary>
/// Returns whether the given instrument name is valid according to the specification.
/// </summary>
/// <remarks>See specification: <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument"/>.</remarks>
/// <param name="instrumentName">The instrument name.</param>
/// <returns>Boolean indicating if the instrument is valid.</returns>
public static bool IsValidInstrumentName(string instrumentName)
{
if (string.IsNullOrWhiteSpace(instrumentName))
{
return false;
}

return InstrumentNameRegex.IsMatch(instrumentName);
}

/// <summary>
/// Returns whether the given custom view name is valid according to the specification.
/// </summary>
/// <remarks>See specification: <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument"/>.</remarks>
/// <param name="customViewName">The view name.</param>
/// <returns>Boolean indicating if the instrument is valid.</returns>
public static bool IsValidViewName(string customViewName)
{
// Only validate the view name in case it's not null. In case it's null, the view name will be the instrument name as per the spec.
if (customViewName == null)
{
return true;
}

return InstrumentNameRegex.IsMatch(customViewName);
}

public void RegisterProvider(MeterProviderSdk meterProvider)
{
Debug.Assert(meterProvider != null, "meterProvider was null");
Expand Down
2 changes: 1 addition & 1 deletion src/OpenTelemetry/Metrics/MeterProviderSdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ internal static void MeasurementRecordedDouble(Instrument instrument, double val

if (viewConfigCount <= 0)
{
if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name))
if (!MetricGuard.IsValidInstrumentName(instrument.Name))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
instrument.Name,
Expand Down
100 changes: 100 additions & 0 deletions src/OpenTelemetry/Metrics/MetricGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace OpenTelemetry.Metrics;

/// <summary>
/// Methods for guarding against exception throwing values in Metrics.
/// </summary>
internal partial class MetricGuard
{
// Note: We don't use static readonly here because some customers
// replace this using reflection which is not allowed on initonly static
// fields. See: https://github.com/dotnet/runtime/issues/11571.
// Customers: This is not guaranteed to work forever. We may change this
// mechanism in the future do this at your own risk.
#if NET
[GeneratedRegex(@"^[a-z][a-z0-9-._/]{0,254}$", RegexOptions.IgnoreCase)]
public static partial Regex InstrumentNameRegex();
#else
private static readonly Regex InstrumentNameRegexField = new(
@"^[a-z][a-z0-9-._/]{0,254}$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

public static Regex InstrumentNameRegex() => InstrumentNameRegexField;
#endif

/// <summary>
/// Throws an exception if the given view name is invalid according to the specification.
/// Null is valid because the instrument name will be used as the view name.
/// </summary>
/// <remarks>See specification: <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument"/>.</remarks>
/// <param name="viewName">The view name.</param>
/// <param name="paramName">The parameter name to use in the thrown exception.</param>
[DebuggerHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfInvalidViewName(string? viewName, [CallerArgumentExpression(nameof(viewName))] string? paramName = null)
{
if (!IsValidViewName(viewName))
{
throw new ArgumentException($"View name {viewName} is invalid.", paramName);
}
}

/// <summary>
/// Throws an exception if the given custom view name is invalid according to the specification.
/// </summary>
/// <remarks>See specification: <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument"/>.</remarks>
/// <param name="viewName">The view name.</param>
/// <param name="paramName">The parameter name to use in the thrown exception.</param>
[DebuggerHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfInvalidCustomViewName(string? viewName, [CallerArgumentExpression(nameof(viewName))] string? paramName = null)
{
if (!IsValidInstrumentName(viewName))
{
throw new ArgumentException($"Custom view name {viewName} is invalid.", paramName);
}
}

/// <summary>
/// Returns whether the given instrument name is valid according to the specification.
/// </summary>
/// <remarks>See specification: <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument"/>.</remarks>
/// <param name="instrumentName">The instrument name.</param>
/// <returns>Boolean indicating if the instrument is valid.</returns>
[DebuggerHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsValidInstrumentName([NotNullWhen(true)] string? instrumentName)
{
if (string.IsNullOrWhiteSpace(instrumentName))
{
return false;
}

return InstrumentNameRegex().IsMatch(instrumentName);
}

/// <summary>
/// Returns whether the given custom view name is valid according to the specification.
/// </summary>
/// <remarks>See specification: <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument"/>.</remarks>
/// <param name="customViewName">The view name.</param>
/// <returns>Boolean indicating if the instrument is valid.</returns>
[DebuggerHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsValidViewName(string? customViewName)
{
// Only validate the view name in case it's not null. In case it's null, the view name will be the instrument name as per the spec.
if (customViewName == null)
{
return true;
}

return InstrumentNameRegex().IsMatch(customViewName);
}
}
23 changes: 17 additions & 6 deletions src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,24 @@ internal virtual List<Metric> AddMetricWithViews(Instrument instrument, List<Met
? this.exemplarFilterForHistograms ?? this.exemplarFilter
: this.exemplarFilter;

if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricStreamIdentity.InstrumentName))
if (!MetricGuard.IsValidInstrumentName(metricStreamIdentity.InstrumentName))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
metricStreamIdentity.InstrumentName,
metricStreamIdentity.MeterName,
"Metric name is invalid.",
"The name must comply with the OpenTelemetry specification.");
if (metricStreamConfig?.Name == null)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
metricStreamIdentity.InstrumentName,
metricStreamIdentity.MeterName,
"Instrument name is invalid.",
"The name must comply with the OpenTelemetry specification.");
}
else
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
metricStreamIdentity.InstrumentName,
metricStreamIdentity.MeterName,
"View name is invalid.",
"The name must comply with the OpenTelemetry specification.");
}

continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ public string? Name
get => this.name;
set
{
if (value != null && !MeterProviderBuilderSdk.IsValidViewName(value))
{
throw new ArgumentException($"Custom view name {value} is invalid.", nameof(value));
}
MetricGuard.ThrowIfInvalidViewName(value);

this.name = value;
}
Expand Down
68 changes: 68 additions & 0 deletions test/OpenTelemetry.Tests/Metrics/MetricGuardTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using OpenTelemetry.Metrics;
using OpenTelemetry.Metrics.Tests;
using Xunit;

namespace OpenTelemetry.Tests.Metrics;

public class MetricGuardTests
{
[Theory]
[MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))]
[InlineData(null)]
public void IsValidInstrumentName_ReturnsFalse_ForInvalidNames(string? instrumentName)
{
Assert.False(MetricGuard.IsValidInstrumentName(instrumentName));
}

[Theory]
[MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))]
public void IsValidInstrumentName_ReturnsTrue_ForValidNames(string instrumentName)
{
Assert.True(MetricGuard.IsValidInstrumentName(instrumentName));
}

[Theory]
[MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))]
public void ThrowIfInvalidViewName_ThrowsOnInvalid(string? viewName)
{
var ex = Assert.Throws<ArgumentException>(() =>
MetricGuard.ThrowIfInvalidViewName(viewName));

Assert.Contains($"View name {viewName} is invalid.", ex.Message, StringComparison.Ordinal);
}

[Theory]
[MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))]
[InlineData(null)] // null is valid because the instrument name will be used as the view name.
public void ThrowIfInvalidViewName_DoesNotThrowForValid(string? viewName)
{
var ex = Record.Exception(() =>
MetricGuard.ThrowIfInvalidViewName(viewName));

Assert.Null(ex);
}

[Theory]
[MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))]
[InlineData(null)] // null is invalid for custom view names.
public void ThrowIfInvalidCustomViewName_ThrowsOnInvalid(string? customViewName)
{
var ex = Assert.Throws<ArgumentException>(() =>
MetricGuard.ThrowIfInvalidCustomViewName(customViewName));

Assert.Contains($"Custom view name {customViewName} is invalid.", ex.Message, StringComparison.Ordinal);
}

[Theory]
[MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))]
public void ThrowIfInvalidCustomViewName_DoesNotThrowForValid(string? customViewName)
{
var ex = Record.Exception(() =>
MetricGuard.ThrowIfInvalidCustomViewName(customViewName));

Assert.Null(ex);
}
}
2 changes: 1 addition & 1 deletion test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName)
.AddView("name1", new MetricStreamConfiguration() { Name = viewNewName })
.AddInMemoryExporter(exportedItems)));

Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message, StringComparison.Ordinal);
Assert.Contains($"View name {viewNewName} is invalid.", ex.Message, StringComparison.Ordinal);
}

[Fact]
Expand Down
Loading