Skip to content

Commit 284e5a3

Browse files
authored
Limit number of console log messages retained in dashboard process (#4936)
* Add LogEntries.MaximumEntryCount Inserting log entries such that the total exceeds this amount results in the earliest log entry being dropped. * Fix typo * Use compound assignment * Remove redundant arguments These always have the same value and can be captured along with other captured state. * Remove redundant using alias * Control console log history limit via configuration * Rename symbol * Update doc comment * Use CircularBuffer and inject config * Define and use central value for config key name
1 parent 5c7f578 commit 284e5a3

File tree

11 files changed

+94
-20
lines changed

11 files changed

+94
-20
lines changed

src/Aspire.Dashboard/Authentication/OpenIdConnect/AuthorizationPolicyBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Authorization;
5-
using OpenIdConnectOptions = Aspire.Dashboard.Configuration.OpenIdConnectOptions;
5+
using Aspire.Dashboard.Configuration;
66

77
namespace Aspire.Dashboard.Authentication.OpenIdConnect;
88

src/Aspire.Dashboard/Components/Controls/LogViewer.razor.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,7 @@ internal async Task SetLogSourceAsync(string resourceName, IAsyncEnumerable<IRea
7373
{
7474
// Keep track of the base line number to ensure that we can calculate the line number of each log entry.
7575
// This becomes important when the total number of log entries exceeds the limit and is truncated.
76-
if (ViewModel.LogEntries.BaseLineNumber is null)
77-
{
78-
ViewModel.LogEntries.BaseLineNumber = lineNumber;
79-
}
76+
ViewModel.LogEntries.BaseLineNumber ??= lineNumber;
8077

8178
ViewModel.LogEntries.InsertSorted(logParser.CreateLogEntry(content, isErrorOutput));
8279
}

src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ async Task TrackResourceSnapshotsAsync()
120120
{
121121
await foreach (var changes in subscription.WithCancellation(_resourceSubscriptionCancellation.Token).ConfigureAwait(false))
122122
{
123-
// TODO: This could be updated to be more efficent.
123+
// TODO: This could be updated to be more efficient.
124124
// It should apply on the resource changes in a batch and then update the UI.
125125
foreach (var (changeType, resource) in changes)
126126
{

src/Aspire.Dashboard/Configuration/DashboardOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ public sealed class FrontendOptions
131131
public string? EndpointUrls { get; set; }
132132
public FrontendAuthMode? AuthMode { get; set; }
133133
public string? BrowserToken { get; set; }
134+
135+
/// <summary>
136+
/// Gets and sets an optional limit on the number of console log messages to be retained in the viewer.
137+
/// </summary>
138+
/// <remarks>
139+
/// The viewer will retain at most this number of log messages. When the limit is reached, the oldest messages will be removed.
140+
/// Defaults to 10,000, which matches the default used in the app host's circular buffer, on the publish side.
141+
/// </remarks>
142+
public int MaxConsoleLogCount { get; set; } = 10_000;
143+
134144
public OpenIdConnectOptions OpenIdConnect { get; set; } = new();
135145

136146
public byte[]? GetBrowserTokenBytes() => _browserTokenBytes;

src/Aspire.Dashboard/Configuration/ValidateDashboardOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public ValidateOptionsResult Validate(string? name, DashboardOptions options)
5555
break;
5656
}
5757

58+
if (options.Frontend.MaxConsoleLogCount <= 0)
59+
{
60+
errorMessages.Add($"{DashboardConfigNames.DashboardFrontendMaxConsoleLogCountName.ConfigKey} must be greater than zero.");
61+
}
62+
5863
if (!options.Otlp.TryParseOptions(out var otlpParseErrorMessage))
5964
{
6065
errorMessages.Add(otlpParseErrorMessage);

src/Aspire.Dashboard/ConsoleLogs/LogEntries.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33

44
using System.Diagnostics;
55
using Aspire.Dashboard.Model;
6+
using Aspire.Dashboard.Otlp.Storage;
67

78
namespace Aspire.Dashboard.ConsoleLogs;
89

9-
public sealed class LogEntries
10+
public sealed class LogEntries(int maximumEntryCount)
1011
{
11-
private readonly List<LogEntry> _logEntries = new();
12+
private readonly CircularBuffer<LogEntry> _logEntries = new(maximumEntryCount);
1213

1314
public int? BaseLineNumber { get; set; }
1415

@@ -28,7 +29,7 @@ public void InsertSorted(LogEntry logEntry)
2829

2930
if (current.Id == logEntry.ParentId && logEntry.LineIndex - 1 == current.LineIndex)
3031
{
31-
InsertLogEntry(_logEntries, rowIndex + 1, logEntry);
32+
InsertAt(rowIndex + 1);
3233
return;
3334
}
3435
}
@@ -45,17 +46,17 @@ public void InsertSorted(LogEntry logEntry)
4546

4647
if (currentTimestamp != null && currentTimestamp <= logEntry.Timestamp)
4748
{
48-
InsertLogEntry(_logEntries, rowIndex + 1, logEntry);
49+
InsertAt(rowIndex + 1);
4950
return;
5051
}
5152
}
5253
}
5354

5455
// If we didn't find a place to insert then append it to the end. This happens with the first entry, but
5556
// could also happen if the logs don't have recognized timestamps.
56-
InsertLogEntry(_logEntries, _logEntries.Count, logEntry);
57+
InsertAt(_logEntries.Count);
5758

58-
void InsertLogEntry(List<LogEntry> logEntries, int index, LogEntry logEntry)
59+
void InsertAt(int index)
5960
{
6061
// Set the line number of the log entry.
6162
if (index == 0)
@@ -65,15 +66,16 @@ void InsertLogEntry(List<LogEntry> logEntries, int index, LogEntry logEntry)
6566
}
6667
else
6768
{
68-
logEntry.LineNumber = logEntries[index - 1].LineNumber + 1;
69+
logEntry.LineNumber = _logEntries[index - 1].LineNumber + 1;
6970
}
7071

71-
logEntries.Insert(index, logEntry);
72+
// Insert the entry.
73+
_logEntries.Insert(index, logEntry);
7274

7375
// If a log entry isn't inserted at the end then update the line numbers of all subsequent entries.
74-
for (var i = index + 1; i < logEntries.Count; i++)
76+
for (var i = index + 1; i < _logEntries.Count; i++)
7577
{
76-
logEntries[i].LineNumber++;
78+
_logEntries[i].LineNumber++;
7779
}
7880
}
7981
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Aspire.Dashboard.Configuration;
45
using Aspire.Dashboard.ConsoleLogs;
6+
using Microsoft.Extensions.Options;
57

68
namespace Aspire.Dashboard.Model;
79

8-
public class LogViewerViewModel
10+
public class LogViewerViewModel(IOptions<DashboardOptions> options)
911
{
10-
public LogEntries LogEntries { get; } = new();
12+
public LogEntries LogEntries { get; } = new(options.Value.Frontend.MaxConsoleLogCount);
1113
public string? ResourceName { get; set; }
12-
1314
}

src/Aspire.Dashboard/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ Set `Dashboard:Frontend:AuthMode` to `OpenIdConnect`, then add the following con
6363
- `Dashboard:Frontend:OpenIdConnect:RequiredClaimType` specifies the (optional) claim that be present for authorized users. Defaults to empty.
6464
- `Dashboard:Frontend:OpenIdConnect:RequiredClaimValue` specifies the (optional) value of the required claim. Only used if `Dashboard:Frontend:OpenIdConnect:RequireClaimType` is also specified. Defaults to empty.
6565

66+
#### Memory limits
67+
68+
- `Dashboard:Frontend:MaxConsoleLogCount` specifies the (optional) maximum number of console log messages to keep in memory. Defaults to 10,000. When the limit is reached, the oldest messages are removed.
69+
6670
### OTLP authentication
6771

6872
The OTLP endpoint can be secured with [client certificate](https://learn.microsoft.com/aspnet/core/security/authentication/certauth) or API key authentication.

src/Shared/DashboardConfigNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal static class DashboardConfigNames
1717
public static readonly ConfigName DashboardOtlpSecondaryApiKeyName = new("Dashboard:Otlp:SecondaryApiKey", "DASHBOARD__OTLP__SECONDARYAPIKEY");
1818
public static readonly ConfigName DashboardFrontendAuthModeName = new("Dashboard:Frontend:AuthMode", "DASHBOARD__FRONTEND__AUTHMODE");
1919
public static readonly ConfigName DashboardFrontendBrowserTokenName = new("Dashboard:Frontend:BrowserToken", "DASHBOARD__FRONTEND__BROWSERTOKEN");
20+
public static readonly ConfigName DashboardFrontendMaxConsoleLogCountName = new("Dashboard:Frontend:MaxConsoleLogCount", "DASHBOARD__FRONTEND__MAXCONSOLELOGCOUNT");
2021
public static readonly ConfigName ResourceServiceClientAuthModeName = new("Dashboard:ResourceServiceClient:AuthMode", "DASHBOARD__RESOURCESERVICECLIENT__AUTHMODE");
2122
public static readonly ConfigName ResourceServiceClientApiKeyName = new("Dashboard:ResourceServiceClient:ApiKey", "DASHBOARD__RESOURCESERVICECLIENT__APIKEY");
2223
}

tests/Aspire.Dashboard.Tests/ConsoleLogsTests/LogEntriesTests.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class LogEntriesTests
1313
public void InsertSorted_OutOfOrderWithSameTimestamp_ReturnInOrder()
1414
{
1515
// Arrange
16-
var logEntries = new LogEntries();
16+
var logEntries = new LogEntries(maximumEntryCount: int.MaxValue);
1717

1818
var timestamp = new DateTimeOffset(2024, 6, 25, 15, 59, 0, TimeSpan.Zero);
1919

@@ -31,4 +31,44 @@ public void InsertSorted_OutOfOrderWithSameTimestamp_ReturnInOrder()
3131
l => Assert.Equal("2", l.Content),
3232
l => Assert.Equal("3", l.Content));
3333
}
34+
35+
[Fact]
36+
public void InsertSorted_TrimsToMaximumEntryCount_Ordered()
37+
{
38+
// Arrange
39+
var logEntries = new LogEntries(maximumEntryCount: 2) { BaseLineNumber = 1 };
40+
41+
var timestamp = DateTimeOffset.UtcNow;
42+
43+
// Act
44+
logEntries.InsertSorted(new LogEntry { Timestamp = timestamp.AddSeconds(1), Content = "1" });
45+
logEntries.InsertSorted(new LogEntry { Timestamp = timestamp.AddSeconds(2), Content = "2" });
46+
logEntries.InsertSorted(new LogEntry { Timestamp = timestamp.AddSeconds(3), Content = "3" });
47+
48+
// Assert
49+
var entries = logEntries.GetEntries();
50+
Assert.Collection(entries,
51+
l => Assert.Equal("2", l.Content),
52+
l => Assert.Equal("3", l.Content));
53+
}
54+
55+
[Fact]
56+
public void InsertSorted_TrimsToMaximumEntryCount_OutOfOrder()
57+
{
58+
// Arrange
59+
var logEntries = new LogEntries(maximumEntryCount: 2) { BaseLineNumber = 1 };
60+
61+
var timestamp = DateTimeOffset.UtcNow;
62+
63+
// Act
64+
logEntries.InsertSorted(new LogEntry { Timestamp = timestamp.AddSeconds(1), Content = "1" });
65+
logEntries.InsertSorted(new LogEntry { Timestamp = timestamp.AddSeconds(3), Content = "3" });
66+
logEntries.InsertSorted(new LogEntry { Timestamp = timestamp.AddSeconds(2), Content = "2" });
67+
68+
// Assert
69+
var entries = logEntries.GetEntries();
70+
Assert.Collection(entries,
71+
l => Assert.Equal("2", l.Content),
72+
l => Assert.Equal("3", l.Content));
73+
}
3474
}

0 commit comments

Comments
 (0)