diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 8c12d1b62bb..e11a75697e8 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -19,6 +19,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using System.Linq; +using System.Text.RegularExpressions; using OpenTelemetry.Internal; using OpenTelemetry.Resources; @@ -87,10 +88,21 @@ internal MeterProviderSdk( } // Setup Listener - var meterSourcesToSubscribe = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var name in meterSources) + Func 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(StringComparer.OrdinalIgnoreCase); + foreach (var meterSource in meterSources) + { + meterSourcesToSubscribe.Add(meterSource); + } + + shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); } this.listener = new MeterListener(); @@ -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(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(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(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(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); } } @@ -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) { @@ -251,6 +264,12 @@ internal MeterProviderSdk( this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state); this.listener.Start(); + + static Regex GetWildcardRegex(IEnumerable 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; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs index 3bd95500084..73dcb3df392 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs @@ -165,6 +165,89 @@ void ProcessExport(Batch 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(); + 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(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(); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(exportedItems); + + if (hasView) + { + meterProviderBuilder.AddView("gauge1", "renamed"); + } + + using var meterProvider = meterProviderBuilder.Build(); + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.True(exportedItems.Count == 0); + } + [Theory] [InlineData(true)] [InlineData(false)]