diff --git a/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs b/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs index 499daaa..f0f7ec9 100644 --- a/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs +++ b/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs @@ -16,6 +16,7 @@ public class ConsoleLogger : ILogger private int _isCursorMovementEnabled = 3; private readonly RepeatLoggerDetector _repeat; + private TagFilterManager? _tagFilterManager; /// /// 高于或等于此级别的日志才会被记录。 @@ -35,7 +36,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } var message = formatter(state, exception); - if (!IsTagEnabled(message)) + if (_tagFilterManager?.IsTagEnabled(message) is false) { return; } @@ -94,11 +95,6 @@ private static void ConsoleMultilineMessage(string message, Func - /// 当前已设置的过滤标签。 - /// - private static ImmutableHashSetString ConsoleFilterTags { get; set; } = []; - /// /// 高于或等于此级别的日志才会被记录。 /// @@ -114,44 +110,10 @@ public ConsoleLogger UseLevel(LogLevel level) /// 命令行参数。 public ConsoleLogger FilterConsoleTagsFromCommandLineArgs(string[] args) { - for (var i = 0; i < args.Length; i++) - { - if (args[i] == "--log-console-tags" && i + 1 < args.Length) - { - ConsoleFilterTags = args[i + 1].Split([',', ';', ' ']).ToImmutableHashSet(); - break; - } - } + _tagFilterManager = TagFilterManager.FromCommandLineArgs(args); return this; } - /// - /// 判断某个日志是否满足当前标签过滤条件。 - /// - /// 要判断的日志原文。 - /// 是否满足过滤条件。 - private static bool IsTagEnabled(string text) - { - if (ConsoleFilterTags.Count is 0) - { - return true; - } - - var start = text.IndexOf('['); - if (start == -1) - { - return true; - } - var end = text.IndexOf(']', start); - if (end == -1) - { - return true; - } - - var tag = text.AsSpan().Slice(start + 1, end - start - 1); - return ConsoleFilterTags.Contains(tag.ToString()); - } - private void ClearAndMoveToLastLine(int repeatCount) { if (_isCursorMovementEnabled > 0 && repeatCount > 2) diff --git a/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs b/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs new file mode 100644 index 0000000..4eec834 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace dotnetCampus.Logging.Writers.Helpers; + +internal class TagFilterManager +{ + public const string LogTagParameterName = "--log-console-tags"; + + /// + /// 当前已设置的任一标签。(无前缀) + /// + public required ImmutableHashSetString AnyFilterTags { get; init; } + + /// + /// 当前已设置的包含标签。(前缀为 +) + /// + public required ImmutableHashSetString IncludingFilterTags { get; init; } + + /// + /// 当前已设置的排除标签。(前缀为 -) + /// + public required ImmutableHashSetString ExcludingFilterTags { get; init; } + + /// + /// 判断某个日志是否满足当前标签过滤条件。 + /// + /// 要判断的日志原文。 + /// 是否满足过滤条件。 + /// + /// 匹配原则: + /// + /// 先看任一标签进行初筛:只要有一个标签匹配,即选出;但如果没有指定任一标签,则全部选出。 + /// 在前一个初筛的基础上,再看排除标签:只要有一个标签匹配,即排除。 + /// 在前两个筛选的基础上,再看包含标签:必须全部标签匹配,才选出,其他全部排除。 + /// + /// + internal bool IsTagEnabled(string text) + { + if (AnyFilterTags.Count is 0 && ExcludingFilterTags.Count is 0 && IncludingFilterTags.Count is 0) + { + return true; + } + + var 任一满足 = AnyFilterTags.Count is 0; + var 包含满足 = IncludingFilterTags.Count is 0; + + var currentTagStartIndex = -1; + var isInTag = false; + List includingTags = IncludingFilterTags.ToList(); + for (var i = 0; i < text.Length; i++) + { + if (text[i] == '[') + { + // 进入标签。 + currentTagStartIndex = i; + isInTag = true; + } + else if (text[i] == ']') + { + // 离开标签。 + var currentTagEndIndex = i; + isInTag = false; + if (currentTagStartIndex < 0) + { + return 任一满足; + } + var tag = text.AsSpan().Slice(currentTagStartIndex + 1, currentTagEndIndex - currentTagStartIndex - 1).ToString(); + // 只要有一个排除标签匹配,就不输出。 + if (ExcludingFilterTags.Contains(tag)) + { + return false; + } + // 如果有任一标签,则匹配一个即可。 + 任一满足 = 任一满足 || AnyFilterTags.Contains(tag); + if (任一满足) + { + // 如果有包含标签,则匹配一个,直到全部匹配。 + if (!包含满足 && IncludingFilterTags.Count > 0) + { + if (includingTags.Contains(tag)) + { + includingTags.Remove(tag); + } + if (includingTags.Count is 0) + { + 包含满足 = true; + } + } + } + } + else if (char.IsWhiteSpace(text[i])) + { + // 空白字符,不处理。 + } + else if (!isInTag) + { + // 当前不在标签内,且非空白字符,直接跳出。 + return 任一满足 && 包含满足; + } + } + return 任一满足 && 包含满足; + } + + /// + /// 从命令行参数中提取过滤标签。 + /// + /// 命令行参数。 + public static TagFilterManager? FromCommandLineArgs(string[] args) + { + HashSet anyFilterTags = []; + HashSet includingFilterTags = []; + HashSet excludingFilterTags = []; + for (var i = 0; i < args.Length; i++) + { + if (args[i] != LogTagParameterName || i + 1 >= args.Length) + { + continue; + } + + var filterTags = args[i + 1].Split([',', ';', ' ']); + foreach (var tag in filterTags) + { + if (tag.StartsWith("-", StringComparison.Ordinal)) + { +#if NET8_0_OR_GREATER + excludingFilterTags.Add(tag[1..]); +#else + excludingFilterTags.Add(tag.Substring(1)); +#endif + } + else if (tag.StartsWith("+", StringComparison.Ordinal)) + { +#if NET8_0_OR_GREATER + includingFilterTags.Add(tag[1..]); +#else + includingFilterTags.Add(tag.Substring(1)); +#endif + } + else + { + anyFilterTags.Add(tag); + } + } + + return new TagFilterManager + { + AnyFilterTags = anyFilterTags.ToImmutableHashSet(), + IncludingFilterTags = includingFilterTags.ToImmutableHashSet(), + ExcludingFilterTags = excludingFilterTags.ToImmutableHashSet(), + }; + } + + return new TagFilterManager + { + AnyFilterTags = [], + IncludingFilterTags = [], + ExcludingFilterTags = [], + }; + } +} diff --git a/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj b/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj index ded2b44..c11f7eb 100644 --- a/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj +++ b/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj @@ -44,6 +44,10 @@ + + + + diff --git a/tests/dotnetCampus.Logger.Tests/TagFilterManagerTests.cs b/tests/dotnetCampus.Logger.Tests/TagFilterManagerTests.cs new file mode 100644 index 0000000..954c51d --- /dev/null +++ b/tests/dotnetCampus.Logger.Tests/TagFilterManagerTests.cs @@ -0,0 +1,223 @@ +using dotnetCampus.Logging.Writers.Helpers; + +namespace dotnetCampus.Logger.Tests; + +[TestClass] +public class TagFilterManagerTests +{ + [TestMethod("单个任一标签,只要有一个标签匹配即允许。")] + public void 单个任一标签() + { + var filter = CreateFilter("Foo"); + Assert.IsTrue(filter.IsTagEnabled("[Foo] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("多个任一标签,只要有一个标签匹配即允许。")] + public void 多个任一标签() + { + var filter = CreateFilter("Foo,Bar"); + Assert.IsTrue(filter.IsTagEnabled("[Foo] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("单个包含标签,只要有一个标签匹配即允许。")] + public void 单个包含标签() + { + var filter = CreateFilter("+Foo"); + Assert.IsTrue(filter.IsTagEnabled("[Foo] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("多个包含标签,必须所有标签都匹配才允许。")] + public void 多个包含标签() + { + var filter = CreateFilter("+Foo,+Bar"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("单个排除标签,任一标签匹配即不允许。")] + public void 单个排除标签() + { + var filter = CreateFilter("-Foo"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("多个排除标签,任一标签匹配即不允许。")] + public void 多个排除标签() + { + var filter = CreateFilter("-Foo,-Bar"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("单个任一和包含标签,只要有一个标签匹配即允许。")] + public void 单个任一和包含标签() + { + var filter = CreateFilter("Foo,+Bar"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("单个任一和排除标签,只要有一个标签匹配即不允许。")] + public void 单个任一和排除标签() + { + var filter = CreateFilter("Foo,-Bar"); + Assert.IsTrue(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("单个包含和排除标签,只要有一个标签匹配即不允许。")] + public void 单个包含和排除标签() + { + var filter = CreateFilter("+Foo,-Bar"); + Assert.IsTrue(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("单个任一和包含和排除标签,只要有一个排除标签匹配即不允许,否则只要有一个标签匹配即允许。")] + public void 单个任一和包含和排除标签() + { + var filter = CreateFilter("Foo,+Bar,-Baz"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Baz] Message")); + Assert.IsTrue(filter.IsTagEnabled("[xxxx][Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo][Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Bar][Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx][Foo][Bar][Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[xxxx] Message")); + } + + [TestMethod("多个任一和包含标签,在任一标签匹配的基础上,必须所有包含标签匹配才允许。")] + public void 多个任一和包含标签() + { + var filter = CreateFilter("Foo,Bar,+Baz,+Qux"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Qux] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Baz] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Baz][Qux] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar][Baz][Qux] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz][Qux] Message")); + } + + [TestMethod("多个任一和排除标签,在任一标签匹配的基础上,只要有一个排除标签匹配即不允许。")] + public void 多个任一和排除标签() + { + var filter = CreateFilter("Foo,Bar,-Baz,-Qux"); + Assert.IsTrue(filter.IsTagEnabled("[Foo] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Qux] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Baz][Qux] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Bar][Baz][Qux] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz][Qux] Message")); + } + + [TestMethod("多个包含和排除标签,必须所有包含标签匹配,且没有排除标签匹配才允许。")] + public void 多个包含和排除标签() + { + var filter = CreateFilter("+Foo,+Bar,-Baz,-Qux"); + Assert.IsFalse(filter.IsTagEnabled("[Foo] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Qux] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo][Bar] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Baz][Qux] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo][Bar][Baz][Qux] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz][Qux] Message")); + } + + public void 多个任一和包含和排除标签() + { + var filter = CreateFilter("Foo1,Foo2,+Bar1,+Bar2,-Baz1,-Baz2"); + Assert.IsFalse(filter.IsTagEnabled("[Foo1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Bar2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Foo2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Baz2] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo1][Bar1][Bar2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar2][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar2][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1][Bar2][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1][Bar2][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1][Baz1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar2][Baz1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo1][Bar1][Bar2][Baz1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Baz2] Message")); + Assert.IsTrue(filter.IsTagEnabled("[Foo2][Bar1][Bar2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar2][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar2][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1][Bar2][Baz1] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1][Bar2][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1][Baz1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar2][Baz1][Baz2] Message")); + Assert.IsFalse(filter.IsTagEnabled("[Foo2][Bar1][Bar2][Baz1][Baz2] Message")); + } + + private static TagFilterManager CreateFilter(string commandLineFilterValue) + { + return TagFilterManager.FromCommandLineArgs([TagFilterManager.LogTagParameterName, commandLineFilterValue])!; + } +} diff --git a/tests/dotnetCampus.Logger.Tests/UnitTest1.cs b/tests/dotnetCampus.Logger.Tests/UnitTest1.cs deleted file mode 100644 index 2b72684..0000000 --- a/tests/dotnetCampus.Logger.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace dotnetCampus.Logger.Tests; - -[TestClass] -public class UnitTest1 -{ - [TestMethod("Add test description here")] - public void TestMethod1() - { - } -} diff --git a/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj b/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj index 902f82c..897ea37 100644 --- a/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj +++ b/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj @@ -18,4 +18,8 @@ + + + +