From b426bdba0e2d605dfe1b503170dc01d74438d7fa Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 8 Oct 2024 09:35:17 +0800 Subject: [PATCH 1/5] Automatically convert URLs in values into links in the dashboard --- .../Stress/Stress.ApiService/Program.cs | 14 ++++++- .../Components/Controls/GridValue.razor | 6 +-- .../Components/Controls/GridValue.razor.cs | 40 +++++++++++++++++++ src/Aspire.Dashboard/ConsoleLogs/LogParser.cs | 27 +++++++++---- src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs | 30 ++++++++++---- src/Aspire.Dashboard/wwwroot/js/app.js | 18 ++++++++- .../ConsoleLogsTests/LogEntriesTests.cs | 13 ++++++ .../ConsoleLogsTests/UrlParserTests.cs | 31 ++++++++++++-- 8 files changed, 154 insertions(+), 25 deletions(-) diff --git a/playground/Stress/Stress.ApiService/Program.cs b/playground/Stress/Stress.ApiService/Program.cs index a40a357eb43..d07e295dc2d 100644 --- a/playground/Stress/Stress.ApiService/Program.cs +++ b/playground/Stress/Stress.ApiService/Program.cs @@ -3,7 +3,9 @@ using System.Diagnostics; using System.Text; +using System.Text.Json.Nodes; using System.Threading.Channels; +using System.Xml.Linq; using Microsoft.AspNetCore.Mvc; using Stress.ApiService; @@ -184,6 +186,8 @@ async IAsyncEnumerable WriteOutput() var xmlWithComments = @""; + var xmlWithUrl = new XElement(new XElement("url", "http://localhost:8080")).ToString(); + // From https://microsoftedge.github.io/Demos/json-dummy-data/ var jsonLarge = File.ReadAllText(Path.Combine("content", "example.json")); @@ -194,6 +198,11 @@ async IAsyncEnumerable WriteOutput() 1 ]"; + var jsonWithUrl = new JsonObject + { + ["url"] = "http://localhost:8080" + }.ToString(); + var sb = new StringBuilder(); for (int i = 0; i < 26; i++) { @@ -203,9 +212,12 @@ async IAsyncEnumerable WriteOutput() logger.LogInformation(@"XML large content: {XmlLarge} XML comment content: {XmlComment} +XML URL content: {XmlUrl} JSON large content: {JsonLarge} JSON comment content: {JsonComment} -Long line content: {LongLines}", xmlLarge, xmlWithComments, jsonLarge, jsonWithComments, sb.ToString()); +JSON URL content: {JsonUrl} +Long line content: {LongLines} +URL content: {UrlContent}", xmlLarge, xmlWithComments, xmlWithUrl, jsonLarge, jsonWithComments, jsonWithUrl, sb.ToString(), "http://localhost:8080"); return "Log with formatted data"; }); diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor b/src/Aspire.Dashboard/Components/Controls/GridValue.razor index 60a243d3fca..0e7f22c3b37 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor @@ -8,13 +8,13 @@ @if (EnableMasking && IsMasked) { - + ●●●●●●●● } else { - + @ContentBeforeValue @if (EnableHighlighting && !string.IsNullOrEmpty(HighlightText)) { @@ -22,7 +22,7 @@ } else { - @Value + @((MarkupString)(_formattedValue ?? string.Empty)) } @ContentAfterValue diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs index 26e4b93d66e..bf1ef8dbac6 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; +using Aspire.Dashboard.ConsoleLogs; using Aspire.Dashboard.Resources; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.JSInterop; namespace Aspire.Dashboard.Components.Controls; @@ -81,10 +84,16 @@ public partial class GridValue [Parameter] public string PostCopyToolTip { get; set; } = null!; + [Inject] + public required IJSRuntime JS { get; init; } + private readonly Icon _maskIcon = new Icons.Regular.Size16.EyeOff(); private readonly Icon _unmaskIcon = new Icons.Regular.Size16.Eye(); + private readonly string _cellTextId = $"celltext-{Guid.NewGuid():N}"; private readonly string _copyId = $"copy-{Guid.NewGuid():N}"; private readonly string _menuAnchorId = $"menu-{Guid.NewGuid():N}"; + private string? _value; + private string? _formattedValue; private bool _isMenuOpen; protected override void OnInitialized() @@ -93,6 +102,37 @@ protected override void OnInitialized() PostCopyToolTip = Loc[nameof(ControlsStrings.GridValueCopied)]; } + protected override void OnParametersSet() + { + if (_value != Value) + { + _value = Value; + + if (UrlParser.TryParse(_value, WebUtility.HtmlEncode, out var modifiedText)) + { + _formattedValue = modifiedText; + } + else + { + _formattedValue = WebUtility.HtmlEncode(_value); + } + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // If the value and formatted value are different then there are hrefs in the text. + // Add a click event to the cell text that stops propagation if a href is clicked. + // This prevents details view from opening when the value is in a main page grid. + if (_value != _formattedValue) + { + await JS.InvokeVoidAsync("setCellTextClickHandler", _cellTextId); + } + } + } + private string GetContainerClass() => EnableMasking ? "container masking-enabled" : "container"; private async Task ToggleMaskStateAsync() diff --git a/src/Aspire.Dashboard/ConsoleLogs/LogParser.cs b/src/Aspire.Dashboard/ConsoleLogs/LogParser.cs index 0c40faf4bb1..63ee8abe4e6 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/LogParser.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/LogParser.cs @@ -31,19 +31,30 @@ public LogEntry CreateLogEntry(string rawText, bool isErrorOutput) timestamp = timestampParseResult.Value.Timestamp.UtcDateTime; } - // 2. HTML Encode the raw text for security purposes - content = WebUtility.HtmlEncode(content); + Func callback = (s) => + { + // This callback is run on text that isn't transformed into a clickable URL. - // 3. Parse the content to look for ANSI Control Sequences and color them if possible - var conversionResult = AnsiParser.ConvertToHtml(content, _residualState); - content = conversionResult.ConvertedText; - _residualState = conversionResult.ResidualState; + // 3a. HTML Encode the raw text for security purposes + var updatedText = WebUtility.HtmlEncode(s); - // 4. Parse the content to look for URLs and make them links if possible - if (UrlParser.TryParse(content, out var modifiedText)) + // 3b. Parse the content to look for ANSI Control Sequences and color them if possible + var conversionResult = AnsiParser.ConvertToHtml(updatedText, _residualState); + updatedText = conversionResult.ConvertedText; + _residualState = conversionResult.ResidualState; + + return updatedText ?? string.Empty; + }; + + // 3. Parse the content to look for URLs and make them links if possible + if (UrlParser.TryParse(content, callback, out var modifiedText)) { content = modifiedText; } + else + { + content = callback(content); + } // 5. Create the LogEntry var logEntry = new LogEntry diff --git a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs index f139cdafd15..8db6a856f44 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs @@ -12,20 +12,22 @@ public static partial class UrlParser { private static readonly Regex s_urlRegEx = GenerateUrlRegEx(); - public static bool TryParse(string? text, [NotNullWhen(true)] out string? modifiedText) + public static bool TryParse(string? text, Func? nonMatchFragmentCallback, [NotNullWhen(true)] out string? modifiedText) { if (text is not null) { var urlMatch = s_urlRegEx.Match(text); - var builder = new StringBuilder(text.Length * 2); + StringBuilder? builder = null; var nextCharIndex = 0; while (urlMatch.Success) { + builder ??= new StringBuilder(text.Length * 2); + if (urlMatch.Index > 0) { - builder.Append(text[(nextCharIndex)..urlMatch.Index]); + AppendNonMatchFragment(builder, nonMatchFragmentCallback, text[(nextCharIndex)..urlMatch.Index]); } var urlStart = urlMatch.Index; @@ -36,11 +38,11 @@ public static bool TryParse(string? text, [NotNullWhen(true)] out string? modifi urlMatch = urlMatch.NextMatch(); } - if (builder.Length > 0) + if (builder?.Length > 0) { if (nextCharIndex < text.Length) { - builder.Append(text[(nextCharIndex)..]); + AppendNonMatchFragment(builder, nonMatchFragmentCallback, text[(nextCharIndex)..]); } modifiedText = builder.ToString(); @@ -50,6 +52,16 @@ public static bool TryParse(string? text, [NotNullWhen(true)] out string? modifi modifiedText = null; return false; + + static void AppendNonMatchFragment(StringBuilder stringBuilder, Func? nonMatchFragmentCallback, string text) + { + if (nonMatchFragmentCallback != null) + { + text = nonMatchFragmentCallback(text); + } + + stringBuilder.Append(text); + } } // Regular expression that detects http/https URLs in a log entry @@ -57,10 +69,12 @@ public static bool TryParse(string? text, [NotNullWhen(true)] out string? modifi // to only http/https URLs // // Explanation: - // /b - Match must start at a word boundary (after whitespace or at the start of the text) + // (?<=m|\\b) - Match must start at a word boundary (after whitespace or at the start of the text) or "m". + // "m" is a trailing character of ANSI escape sequences. + // Required because URLs are matched before processing ANSI escape sequences. // https?:// - http:// or https:// // [-A-Za-z0-9+&@#/%?=~_|$!:,.;]* - Any character in the list, matched zero or more times. // [A-Za-z0-9+&@#/%=~_|$] - Any character in the list, matched exactly once - [GeneratedRegex("\\bhttps?://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$]")] - private static partial Regex GenerateUrlRegEx(); + [GeneratedRegex("(?<=m|\\b)https?://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$]")] + public static partial Regex GenerateUrlRegEx(); } diff --git a/src/Aspire.Dashboard/wwwroot/js/app.js b/src/Aspire.Dashboard/wwwroot/js/app.js index 5e0939e04f8..f26fe781674 100644 --- a/src/Aspire.Dashboard/wwwroot/js/app.js +++ b/src/Aspire.Dashboard/wwwroot/js/app.js @@ -355,4 +355,20 @@ window.registerOpenTextVisualizerOnClick = function(layout) { window.unregisterOpenTextVisualizerOnClick = function (obj) { document.removeEventListener('click', obj.onClickListener); -} +}; + +window.setCellTextClickHandler = function (id) { + var cellTextElement = document.getElementById(id); + if (!cellTextElement) { + return; + } + + cellTextElement.addEventListener('click', e => { + // Propagation behavior: + // - Link click stops. Link will open in a new window. + // - Any other text allows propagation. Potentially opens details view. + if (isElementTagName(e.target, 'a')) { + e.stopPropagation(); + } + }); +}; diff --git a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/LogEntriesTests.cs b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/LogEntriesTests.cs index bbd2765b74e..2bf03f9a7f0 100644 --- a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/LogEntriesTests.cs +++ b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/LogEntriesTests.cs @@ -233,4 +233,17 @@ public void InsertSorted_TrimsToMaximumEntryCount_OutOfOrder() l => Assert.Equal("2", l.Content), l => Assert.Equal("3", l.Content)); } + + [Fact] + public void CreateLogEntry_AnsiAndUrl_HasUrlAnchor() + { + // Arrange + var parser = new LogParser(); + + // Act + var entry = parser.CreateLogEntry("\x1b[36mhttps://www.example.com\u001b[0m", isErrorOutput: false); + + // Assert + Assert.Equal("https://www.example.com", entry.Content); + } } diff --git a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs index 3f8e4b32713..18140cd71cd 100644 --- a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs +++ b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; using Aspire.Dashboard.ConsoleLogs; using Xunit; @@ -15,7 +16,7 @@ public class UrlParserTests [InlineData("This is some text without any urls")] public void TryParse_NoUrl_ReturnsFalse(string? input) { - var result = UrlParser.TryParse(input, out var _); + var result = UrlParser.TryParse(input, WebUtility.HtmlEncode, out var _); Assert.False(result); } @@ -26,7 +27,7 @@ public void TryParse_NoUrl_ReturnsFalse(string? input) [InlineData("This is some text with a https://bing.com/ in the middle", true, "This is some text with a https://bing.com/ in the middle")] public void TryParse_ReturnsCorrectResult(string input, bool expectedResult, string? expectedOutput) { - var result = UrlParser.TryParse(input, out var modifiedText); + var result = UrlParser.TryParse(input, WebUtility.HtmlEncode, out var modifiedText); Assert.Equal(expectedResult, result); Assert.Equal(expectedOutput, modifiedText); @@ -42,7 +43,7 @@ public void TryParse_ReturnsCorrectResult(string input, bool expectedResult, str [InlineData("http://bing", "http://bing")] public void TryParse_SupportedUrlFormats(string input, string? expectedOutput) { - var result = UrlParser.TryParse(input, out var modifiedText); + var result = UrlParser.TryParse(input, WebUtility.HtmlEncode, out var modifiedText); Assert.True(result); Assert.Equal(expectedOutput, modifiedText); @@ -54,8 +55,30 @@ public void TryParse_SupportedUrlFormats(string input, string? expectedOutput) [InlineData("ftp://user:pass@ftp.localhost.com/")] public void TryParse_UnsupportedUrlFormats(string input) { - var result = UrlParser.TryParse(input, out var _); + var result = UrlParser.TryParse(input, WebUtility.HtmlEncode, out var _); Assert.False(result); } + + [Theory] + [InlineData("http://localhost:8080", "http://localhost:8080</url>")] + [InlineData("http://localhost:8080\"", "http://localhost:8080"")] + public void TryParse_ExcludeInvalidTrailingChars(string input, string? expectedOutput) + { + var result = UrlParser.TryParse(input, WebUtility.HtmlEncode, out var modifiedText); + Assert.True(result); + + Assert.Equal(expectedOutput, modifiedText); + } + + [Theory] + [InlineData("http://www.localhost:8080")] + [InlineData("mhttp://www.localhost:8080")] + [InlineData(" http://www.localhost:8080")] + public void GenerateUrlRegEx_MatchUrlAfterContent(string content) + { + var regex = UrlParser.GenerateUrlRegEx(); + var match = regex.Match(content); + Assert.Equal("http://www.localhost:8080", match.Value); + } } From 409cf35f271aabb3f71fc225721e137d48e1c4ce Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 22 Oct 2024 16:28:34 +0800 Subject: [PATCH 2/5] Only add click handled when required --- playground/Stress/Stress.ApiService/Program.cs | 5 ++++- src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs | 5 ++++- .../ResourcesGridColumns/LogMessageColumnDisplay.razor | 3 ++- .../ResourcesGridColumns/SourceColumnDisplay.razor | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/playground/Stress/Stress.ApiService/Program.cs b/playground/Stress/Stress.ApiService/Program.cs index d07e295dc2d..086e6fafb9c 100644 --- a/playground/Stress/Stress.ApiService/Program.cs +++ b/playground/Stress/Stress.ApiService/Program.cs @@ -217,7 +217,10 @@ async IAsyncEnumerable WriteOutput() JSON comment content: {JsonComment} JSON URL content: {JsonUrl} Long line content: {LongLines} -URL content: {UrlContent}", xmlLarge, xmlWithComments, xmlWithUrl, jsonLarge, jsonWithComments, jsonWithUrl, sb.ToString(), "http://localhost:8080"); +URL content: {UrlContent} +Empty content: {EmptyContent} +Whitespace content: {WhitespaceContent} +Null content: {NullContent}", xmlLarge, xmlWithComments, xmlWithUrl, jsonLarge, jsonWithComments, jsonWithUrl, sb.ToString(), "http://localhost:8080", "", " ", null); return "Log with formatted data"; }); diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs index bf1ef8dbac6..5ca4f3f142a 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor.cs @@ -84,6 +84,9 @@ public partial class GridValue [Parameter] public string PostCopyToolTip { get; set; } = null!; + [Parameter] + public bool StopClickPropagation { get; set; } + [Inject] public required IJSRuntime JS { get; init; } @@ -126,7 +129,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) // If the value and formatted value are different then there are hrefs in the text. // Add a click event to the cell text that stops propagation if a href is clicked. // This prevents details view from opening when the value is in a main page grid. - if (_value != _formattedValue) + if (StopClickPropagation && _value != _formattedValue) { await JS.InvokeVoidAsync("setCellTextClickHandler", _cellTextId); } diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor index a7b3ae239a7..8170fc4be37 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor @@ -9,7 +9,8 @@ + HighlightText="@FilterText" + StopClickPropagation="true"> diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor index c73d6b8c45e..a8857ef3579 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor @@ -10,7 +10,8 @@ EnableHighlighting="true" HighlightText="@FilterText" PreCopyToolTip="@Loc[nameof(Columns.SourceColumnDisplayCopyCommandToClipboard)]" - ToolTip="@Tooltip"> + ToolTip="@Tooltip" + StopClickPropagation="true"> @if (ContentAfterValue is not null) { From 9b14e5790934082a67ac34bcc0d6aa9d8b0b7df2 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 23 Oct 2024 08:32:57 +0800 Subject: [PATCH 3/5] PR feedback --- src/Aspire.Dashboard/Components/Controls/GridValue.razor | 4 ++-- src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs | 4 +++- .../ConsoleLogsTests/UrlParserTests.cs | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/GridValue.razor b/src/Aspire.Dashboard/Components/Controls/GridValue.razor index 0e7f22c3b37..0627f6153db 100644 --- a/src/Aspire.Dashboard/Components/Controls/GridValue.razor +++ b/src/Aspire.Dashboard/Components/Controls/GridValue.razor @@ -20,9 +20,9 @@ { } - else + else if (_formattedValue != null) { - @((MarkupString)(_formattedValue ?? string.Empty)) + @((MarkupString)_formattedValue) } @ContentAfterValue diff --git a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs index 8db6a856f44..0a5f48d6014 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs @@ -75,6 +75,8 @@ static void AppendNonMatchFragment(StringBuilder stringBuilder, Func Date: Wed, 23 Oct 2024 08:34:43 +0800 Subject: [PATCH 4/5] Experimenting --- tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs index 9283ae89f66..d7a5ce4023a 100644 --- a/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs +++ b/tests/Aspire.Dashboard.Tests/ConsoleLogsTests/UrlParserTests.cs @@ -76,6 +76,8 @@ public void TryParse_ExcludeInvalidTrailingChars(string input, string? expectedO // thequickhttp://localhost:80#brownfox // mhttp://localhost:80#brownfox // httphttp://localhost:80#brownfox + // http://WWW.localhost:80#brownfox + // HTTP://www.localhost:80#brownfox [Theory] [InlineData("http://www.localhost:8080")] [InlineData("HTTP://WWW.LOCALHOST:8080")] From 36f10b19a28f7b3013940e02c3785da1a3478e11 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 23 Oct 2024 08:53:15 +0800 Subject: [PATCH 5/5] Update --- .../ConsoleLogs/LogLevelParser.cs | 27 ----------------- src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs | 13 ++++----- .../ConsoleLogsTests/LogLevelParserTests.cs | 29 ------------------- .../ConsoleLogsTests/UrlParserTests.cs | 8 +---- 4 files changed, 7 insertions(+), 70 deletions(-) delete mode 100644 src/Aspire.Dashboard/ConsoleLogs/LogLevelParser.cs delete mode 100644 tests/Aspire.Dashboard.Tests/ConsoleLogsTests/LogLevelParserTests.cs diff --git a/src/Aspire.Dashboard/ConsoleLogs/LogLevelParser.cs b/src/Aspire.Dashboard/ConsoleLogs/LogLevelParser.cs deleted file mode 100644 index b0afbcaaaee..00000000000 --- a/src/Aspire.Dashboard/ConsoleLogs/LogLevelParser.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.RegularExpressions; - -namespace Aspire.Dashboard.ConsoleLogs; - -public static partial class LogLevelParser -{ - private static readonly Regex s_logLevelRegex = GenerateLogLevelRegex(); - - public static bool StartsWithLogLevelHeader(string text) => s_logLevelRegex.IsMatch(text); - - // Regular expression that detects log levels used as indicators - // of the first line of a log entry, skipping any ANSI control sequences - // that may come first. - [GeneratedRegex( - """ - ^ # start of string - (\x1B\[\d{1,2}m)* # zero or more ANSI control sequences - (trce|dbug|info|warn|fail|crit) # one of the log level names - (\x1B\[\d{1,2}m)* # zero or more ANSI control sequences - : # colon, followed by arbitrary content that is not matched - """, - RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace)] - private static partial Regex GenerateLogLevelRegex(); -} diff --git a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs index 0a5f48d6014..0b19a200baa 100644 --- a/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs +++ b/src/Aspire.Dashboard/ConsoleLogs/UrlParser.cs @@ -65,18 +65,17 @@ static void AppendNonMatchFragment(StringBuilder stringBuilder, Func