Skip to content

Commit

Permalink
Added wildcard support for meter sources. (#2459)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yun-Ting authored Oct 9, 2021
1 parent 63460be commit c79ad98
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 96 deletions.
211 changes: 115 additions & 96 deletions src/OpenTelemetry/Metrics/MeterProviderSdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text.RegularExpressions;
using OpenTelemetry.Internal;
using OpenTelemetry.Resources;

Expand Down Expand Up @@ -87,10 +88,21 @@ internal MeterProviderSdk(
}

// Setup Listener
var meterSourcesToSubscribe = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
foreach (var name in meterSources)
Func<Instrument, bool> shouldListenTo = instrument => false;
if (meterSources.Any(s => s.Contains('*')))
{
meterSourcesToSubscribe[name] = true;
var regex = GetWildcardRegex(meterSources);
shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name);
}
else if (meterSources.Any())
{
var meterSourcesToSubscribe = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var meterSource in meterSources)
{
meterSourcesToSubscribe.Add(meterSource);
}

shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name);
}

this.listener = new MeterListener();
Expand All @@ -99,80 +111,82 @@ internal MeterProviderSdk(
{
this.listener.InstrumentPublished = (instrument, listener) =>
{
if (meterSourcesToSubscribe.ContainsKey(instrument.Meter.Name))
if (!shouldListenTo(instrument))
{
// Creating list with initial capacity as the maximum
// possible size, to avoid any array resize/copy internally.
// There may be excess space wasted, but it'll eligible for
// GC right after this method.
var metricStreamConfigs = new List<MetricStreamConfiguration>(viewConfigCount);
foreach (var viewConfig in this.viewConfigs)
{
var metricStreamConfig = viewConfig(instrument);
if (metricStreamConfig != null)
{
metricStreamConfigs.Add(metricStreamConfig);
}
}
return;
}

if (metricStreamConfigs.Count == 0)
// Creating list with initial capacity as the maximum
// possible size, to avoid any array resize/copy internally.
// There may be excess space wasted, but it'll eligible for
// GC right after this method.
var metricStreamConfigs = new List<MetricStreamConfiguration>(viewConfigCount);
foreach (var viewConfig in this.viewConfigs)
{
var metricStreamConfig = viewConfig(instrument);
if (metricStreamConfig != null)
{
// No views matched. Add null
// which will apply defaults.
// Users can turn off this default
// by adding a view like below as the last view.
// .AddView(instrumentName: "*", new MetricStreamConfiguration() { Aggregation = Aggregation.Drop })
metricStreamConfigs.Add(null);
metricStreamConfigs.Add(metricStreamConfig);
}
}

var maxCountMetricsToBeCreated = metricStreamConfigs.Count;
if (metricStreamConfigs.Count == 0)
{
// No views matched. Add null
// which will apply defaults.
// Users can turn off this default
// by adding a view like below as the last view.
// .AddView(instrumentName: "*", new MetricStreamConfiguration() { Aggregation = Aggregation.Drop })
metricStreamConfigs.Add(null);
}

// Create list with initial capacity as the max metric count.
// Due to duplicate/max limit, we may not end up using them
// all, and that memory is wasted until Meter disposed.
// TODO: Revisit to see if we need to do metrics.TrimExcess()
var metrics = new List<Metric>(maxCountMetricsToBeCreated);
lock (this.instrumentCreationLock)
var maxCountMetricsToBeCreated = metricStreamConfigs.Count;

// Create list with initial capacity as the max metric count.
// Due to duplicate/max limit, we may not end up using them
// all, and that memory is wasted until Meter disposed.
// TODO: Revisit to see if we need to do metrics.TrimExcess()
var metrics = new List<Metric>(maxCountMetricsToBeCreated);
lock (this.instrumentCreationLock)
{
for (int i = 0; i < maxCountMetricsToBeCreated; i++)
{
for (int i = 0; i < maxCountMetricsToBeCreated; i++)
var metricStreamConfig = metricStreamConfigs[i];
var metricStreamName = metricStreamConfig?.Name ?? instrument.Name;
if (this.metricStreamNames.ContainsKey(metricStreamName))
{
// TODO: Log that instrument is ignored
// as the resulting Metric name is conflicting
// with existing name.
continue;
}

if (metricStreamConfig?.Aggregation == Aggregation.Drop)
{
var metricStreamConfig = metricStreamConfigs[i];
var metricStreamName = metricStreamConfig?.Name ?? instrument.Name;
if (this.metricStreamNames.ContainsKey(metricStreamName))
{
// TODO: Log that instrument is ignored
// as the resulting Metric name is conflicting
// with existing name.
continue;
}

if (metricStreamConfig?.Aggregation == Aggregation.Drop)
{
// TODO: Log that instrument is ignored
// as user explicitly asked to drop it
// with View.
continue;
}

var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
// TODO: Log that instrument is ignored
// as max number of Metrics have reached.
}
else
{
Metric metric;
var metricDescription = metricStreamConfig?.Description ?? instrument.Description;
string[] tagKeysInteresting = metricStreamConfig?.TagKeys;
double[] histogramBucketBounds = (metricStreamConfig is HistogramConfiguration histogramConfig
&& histogramConfig.BucketBounds != null) ? histogramConfig.BucketBounds : null;
metric = new Metric(instrument, temporality, metricStreamName, metricDescription, histogramBucketBounds, tagKeysInteresting);

this.metrics[index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamName, true);
}
// TODO: Log that instrument is ignored
// as user explicitly asked to drop it
// with View.
continue;
}

var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
// TODO: Log that instrument is ignored
// as max number of Metrics have reached.
}
else
{
Metric metric;
var metricDescription = metricStreamConfig?.Description ?? instrument.Description;
string[] tagKeysInteresting = metricStreamConfig?.TagKeys;
double[] histogramBucketBounds = (metricStreamConfig is HistogramConfiguration histogramConfig
&& histogramConfig.BucketBounds != null) ? histogramConfig.BucketBounds : null;
metric = new Metric(instrument, temporality, metricStreamName, metricDescription, histogramBucketBounds, tagKeysInteresting);

this.metrics[index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamName, true);
}
}

Expand All @@ -197,40 +211,39 @@ internal MeterProviderSdk(
{
this.listener.InstrumentPublished = (instrument, listener) =>
{
if (!shouldListenTo(instrument))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider.");
return;
}

try
{
if (meterSourcesToSubscribe.ContainsKey(instrument.Meter.Name))
var metricName = instrument.Name;
Metric metric = null;
lock (this.instrumentCreationLock)
{
var metricName = instrument.Name;
Metric metric = null;
lock (this.instrumentCreationLock)
if (this.metricStreamNames.ContainsKey(metricName))
{
if (this.metricStreamNames.ContainsKey(metricName))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using View.");
return;
}

var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Maximum allowed Metrics for the provider exceeded.", "Use views to drop unused instruments. Or configure Provider to allow higher limit.");
return;
}
else
{
metric = new Metric(instrument, temporality, metricName, instrument.Description);
this.metrics[index] = metric;
this.metricStreamNames.Add(metricName, true);
}
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using View.");
return;
}

listener.EnableMeasurementEvents(instrument, metric);
}
else
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider.");
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Maximum allowed Metrics for the provider exceeded.", "Use views to drop unused instruments. Or configure Provider to allow higher limit.");
return;
}
else
{
metric = new Metric(instrument, temporality, metricName, instrument.Description);
this.metrics[index] = metric;
this.metricStreamNames.Add(metricName, true);
}
}

listener.EnableMeasurementEvents(instrument, metric);
}
catch (Exception)
{
Expand All @@ -251,6 +264,12 @@ internal MeterProviderSdk(

this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state);
this.listener.Start();

static Regex GetWildcardRegex(IEnumerable<string> collection)
{
var pattern = '^' + string.Join("|", from name in collection select "(?:" + Regex.Escape(name).Replace("\\*", ".*") + ')') + '$';
return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
}

internal Resource Resource { get; }
Expand Down
83 changes: 83 additions & 0 deletions test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,89 @@ void ProcessExport(Batch<Metric> batch)
Assert.Equal(1, metricCount);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void MeterSourcesWildcardSupportMatchTest(bool hasView)
{
var meterNames = new[]
{
"AbcCompany.XyzProduct.ComponentA",
"abcCompany.xYzProduct.componentC", // Wildcard match is case insensitive.
"DefCompany.AbcProduct.ComponentC",
"DefCompany.XyzProduct.ComponentC", // Wildcard match supports matching multiple patterns.
"GhiCompany.qweProduct.ComponentN",
"SomeCompany.SomeProduct.SomeComponent",
};

using var meter1 = new Meter(meterNames[0]);
using var meter2 = new Meter(meterNames[1]);
using var meter3 = new Meter(meterNames[2]);
using var meter4 = new Meter(meterNames[3]);
using var meter5 = new Meter(meterNames[4]);
using var meter6 = new Meter(meterNames[5]);

var exportedItems = new List<Metric>();
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.AddMeter("AbcCompany.XyzProduct.*")
.AddMeter("DefCompany.*.ComponentC")
.AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name.
.AddInMemoryExporter(exportedItems);

if (hasView)
{
meterProviderBuilder.AddView("myGauge1", "newName");
}

using var meterProvider = meterProviderBuilder.Build();

var measurement = new Measurement<int>(100, new("name", "apple"), new("color", "red"));
meter1.CreateObservableGauge("myGauge1", () => measurement);
meter2.CreateObservableGauge("myGauge2", () => measurement);
meter3.CreateObservableGauge("myGauge3", () => measurement);
meter4.CreateObservableGauge("myGauge4", () => measurement);
meter5.CreateObservableGauge("myGauge5", () => measurement);
meter6.CreateObservableGauge("myGauge6", () => measurement);

meterProvider.ForceFlush(MaxTimeToAllowForFlush);

Assert.True(exportedItems.Count == 5); // "SomeCompany.SomeProduct.SomeComponent" will not be subscribed.

if (hasView)
{
Assert.Equal("newName", exportedItems[0].Name);
}
else
{
Assert.Equal("myGauge1", exportedItems[0].Name);
}

Assert.Equal("myGauge2", exportedItems[1].Name);
Assert.Equal("myGauge3", exportedItems[2].Name);
Assert.Equal("myGauge4", exportedItems[3].Name);
Assert.Equal("myGauge5", exportedItems[4].Name);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void MeterSourcesWildcardSupportWithoutAddingMeterToProvider(bool hasView)
{
var exportedItems = new List<Metric>();
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.AddInMemoryExporter(exportedItems);

if (hasView)
{
meterProviderBuilder.AddView("gauge1", "renamed");
}

using var meterProvider = meterProviderBuilder.Build();
var measurement = new Measurement<int>(100, new("name", "apple"), new("color", "red"));
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.True(exportedItems.Count == 0);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down

0 comments on commit c79ad98

Please sign in to comment.