[release/8.0-rc1] Unify with logging category matching (#90642)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Wed, 16 Aug 2023 17:58:54 +0000 (11:58 -0600)
committerGitHub <noreply@github.com>
Wed, 16 Aug 2023 17:58:54 +0000 (11:58 -0600)
* Unify with logging category matching

* -1 => 0

* Weird overlapping match

---------

Co-authored-by: Chris R <Tratcher@outlook.com>
src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/ListenerSubscription.cs
src/libraries/Microsoft.Extensions.Diagnostics/src/Resources/Strings.resx
src/libraries/Microsoft.Extensions.Diagnostics/tests/ListenerSubscriptionTests.cs

index 24c6d0b..c6eb978 100644 (file)
@@ -165,36 +165,39 @@ namespace Microsoft.Extensions.Diagnostics.Metrics
 
             // Meter
 
-            var ruleMeterName = rule.MeterName.AsSpan();
-            // Don't allow "*" anywhere except at the end.
-            var starIndex = ruleMeterName.IndexOf('*');
-            if (starIndex != -1 && starIndex != ruleMeterName.Length - 1)
+            // The same logic as Microsoft.Extensions.Logging.LoggerRuleSelector.IsBetter for category names
+            var meterName = rule.MeterName;
+            if (meterName != null)
             {
-                return false;
-            }
-            // Rule "System.Net.*" matches meter "System.Net" and "System.Net.Http"
-            if (ruleMeterName.EndsWith(".*".AsSpan(), StringComparison.Ordinal))
-            {
-                ruleMeterName = ruleMeterName.Slice(0, ruleMeterName.Length - 2);
-            }
-            // System.Net* matches System.Net and System.Net.Http
-            else if (starIndex != -1)
-            {
-                ruleMeterName = ruleMeterName.Slice(0, ruleMeterName.Length - 1);
-            }
+                const char WildcardChar = '*';
 
-            // Rule "" matches everything
-            if (ruleMeterName.IsEmpty)
-            {
-                return true;
+                int wildcardIndex = meterName.IndexOf(WildcardChar);
+                if (wildcardIndex >= 0 &&
+                    meterName.IndexOf(WildcardChar, wildcardIndex + 1) >= 0)
+                {
+                    throw new InvalidOperationException(SR.MoreThanOneWildcard);
+                }
+
+                ReadOnlySpan<char> prefix, suffix;
+                if (wildcardIndex < 0)
+                {
+                    prefix = meterName.AsSpan();
+                    suffix = default;
+                }
+                else
+                {
+                    prefix = meterName.AsSpan(0, wildcardIndex);
+                    suffix = meterName.AsSpan(wildcardIndex + 1);
+                }
+
+                if (!instrument.Meter.Name.AsSpan().StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ||
+                    !instrument.Meter.Name.AsSpan().EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
             }
 
-            // "System.Net" matches "System.Net" and "System.Net.Http"
-            return instrument.Meter.Name.AsSpan().StartsWith(ruleMeterName, StringComparison.OrdinalIgnoreCase)
-                // Exact match +/- ".*"
-                && (ruleMeterName.Length == instrument.Meter.Name.Length
-                    // Only allow StartsWith on segment boundaries
-                    || instrument.Meter.Name[ruleMeterName.Length] == '.');
+            return true;
         }
 
         // Everything must already match the Instrument and listener, or be blank.
index ab0d5ca..0589f84 100644 (file)
@@ -1,4 +1,64 @@
-<root>
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
     <xsd:element name="root" msdata:IsDataSet="true">
   <data name="InvalidScope" xml:space="preserve">
     <value>The meter factory does not allow a custom scope value when creating a meter.</value>
   </data>
-</root>
+  <data name="MoreThanOneWildcard" xml:space="preserve">
+    <value>Only one wildcard character is allowed in category name.</value>
+  </data>
+</root>
\ No newline at end of file
index da59cff..a0c5d28 100644 (file)
@@ -214,13 +214,20 @@ namespace Microsoft.Extensions.Diagnostics.Tests
         [InlineData("", "", "")]
         [InlineData("*", "", "")]
         [InlineData("lonG", "", "")]
+        [InlineData("lonG.", "", "")]
         [InlineData("lonG*", "", "")]
         [InlineData("lonG.*", "", "")]
+        [InlineData("lonG.sil", "", "")]
+        [InlineData("lonG.sil*", "", "")]
         [InlineData("lonG.sillY.meteR", "", "")]
         [InlineData("lonG.sillY.meteR*", "", "")]
         [InlineData("lonG.sillY.meteR.*", "", "")]
+        [InlineData("*namE", "", "")]
+        [InlineData("*.namE", "", "")]
+        [InlineData("*.sillY.meteR.Name", "", "")]
+        [InlineData("long*Name", "", "")]
+        [InlineData("lonG.sillY.meter*MeteR.namE", "", "")] // Shouldn't match, but does, left for compatibility with Logging.
         [InlineData("lonG.sillY.meteR.namE", "", "")]
-        [InlineData("lonG.sillY.meteR.namE.*", "", "")]
         [InlineData("", "instrumenTnamE", "")]
         [InlineData("lonG.sillY.meteR.namE", "instrumenTnamE", "")]
         [InlineData("", "", "listeneRnamE")]
@@ -237,15 +244,13 @@ namespace Microsoft.Extensions.Diagnostics.Tests
         }
 
         [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
-        [InlineData("*.*", "", "")]
         [InlineData("", "*", "")]
         [InlineData("", "", "*")]
-        [InlineData("lonG.", "", "")]
-        [InlineData("lonG.sil", "", "")]
-        [InlineData("lonG.sil*", "", "")]
         [InlineData("sillY.meteR.namE", "", "")]
+        [InlineData(".*", "", "")]
+        [InlineData("*.", "", "")]
+        [InlineData("lonG.sillY.meteR.namE.*", "", "")]
         [InlineData("namE", "", "")]
-        [InlineData("*.namE", "", "")]
         [InlineData("wrongMeter", "", "")]
         [InlineData("wrongMeter", "InstrumentName", "")]
         [InlineData("wrongMeter", "", "ListenerName")]
@@ -261,6 +266,17 @@ namespace Microsoft.Extensions.Diagnostics.Tests
             }, meterName, instrumentName, listenerName).Dispose();
         }
 
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void MultipleWildcardsThrows()
+        {
+            RemoteExecutor.Invoke(() => {
+                var rule = new InstrumentRule("*.*", null, null, MeterScope.Global, enable: true);
+                var meter = new Meter("Long.Silly.Meter.Name");
+                var instrument = meter.CreateCounter<int>("InstrumentName");
+                Assert.Throws< InvalidOperationException>(() => ListenerSubscription.RuleMatches(rule, instrument, "ListenerName", new FakeMeterFactory()));
+            }).Dispose();
+        }
+
         [Theory]
         [MemberData(nameof(IsMoreSpecificTestData))]
         public void IsMoreSpecificTest(InstrumentRule rule, InstrumentRule? best, bool isLocalScope)
@@ -388,3 +404,8 @@ namespace Microsoft.Extensions.Diagnostics.Tests
         }
     }
 }
+
+internal class SR
+{
+    public static string MoreThanOneWildcard => "More than one wildcard is not allowed in a rule.";
+}