Skip to content

Commit f5d9f80

Browse files
authored
Simplify timestamp display in console logs (#5455)
1 parent 4ee91ed commit f5d9f80

File tree

9 files changed

+41
-41
lines changed

9 files changed

+41
-41
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@namespace Aspire.Dashboard.Components
2+
@using System.Globalization
23
@using Aspire.Dashboard.Model
4+
@using Aspire.Dashboard.Utils
35
@using Aspire.Hosting.ConsoleLogs
46
@inject IJSRuntime JS
57
@implements IAsyncDisposable
@@ -12,15 +14,20 @@
1214
<span class="log-line-area" role="log">
1315
<span class="log-line-number">@context.LineNumber</span>
1416
<span class="log-content">
17+
@{
18+
var hasPrefix = false;
19+
}
1520
@if (context.Timestamp is { } timestamp)
1621
{
17-
<span class="timestamp">@GetDisplayTimestamp(timestamp)</span>
22+
hasPrefix = true;
23+
<span class="timestamp" title="@FormatHelpers.FormatDateTime(TimeProvider, timestamp, MillisecondsDisplay.Full, CultureInfo.CurrentCulture)">@GetDisplayTimestamp(timestamp)</span>
1824
}
1925
@if (context.Type == LogEntryType.Error)
2026
{
27+
hasPrefix = true;
2128
<fluent-badge appearance="accent">stderr</fluent-badge>
2229
}
23-
@((MarkupString)(context.Content ?? string.Empty))
30+
@((MarkupString)((hasPrefix ? "&#32;" : string.Empty) + (context.Content ?? string.Empty)))
2431
</span>
2532
</span>
2633
</div>

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ private void OnBrowserResize(object? o, EventArgs args)
6565
});
6666
}
6767

68-
internal async Task SetLogSourceAsync(string resourceName, IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> batches, bool convertTimestampsFromUtc)
68+
internal async Task SetLogSourceAsync(string resourceName, IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> batches, bool convertTimestampsFromUtc = true)
6969
{
7070
ResourceName = resourceName;
7171

@@ -102,12 +102,9 @@ internal async Task SetLogSourceAsync(string resourceName, IAsyncEnumerable<IRea
102102

103103
private string GetDisplayTimestamp(DateTimeOffset timestamp)
104104
{
105-
if (_convertTimestampsFromUtc)
106-
{
107-
timestamp = TimeProvider.ToLocal(timestamp);
108-
}
105+
var date = _convertTimestampsFromUtc ? TimeProvider.ToLocal(timestamp) : timestamp.DateTime;
109106

110-
return timestamp.ToString(KnownFormats.ConsoleLogsTimestampFormat, CultureInfo.InvariantCulture);
107+
return date.ToString(KnownFormats.ConsoleLogsUITimestampFormat, CultureInfo.InvariantCulture);
111108
}
112109

113110
internal async Task ClearLogsAsync()

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,7 @@ private async ValueTask LoadLogsAsync()
278278

279279
if (subscription is not null)
280280
{
281-
var task = _logViewer.SetLogSourceAsync(
282-
PageViewModel.SelectedResource.Name,
283-
subscription,
284-
convertTimestampsFromUtc: PageViewModel.SelectedResource.IsContainer());
281+
var task = _logViewer.SetLogSourceAsync(PageViewModel.SelectedResource.Name, subscription);
285282

286283
PageViewModel.InitialisedSuccessfully = true;
287284
PageViewModel.Status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsWatchingLogs)];

src/Aspire.Hosting/Dashboard/ConsoleLogsConfigurationExtensions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ internal static IResourceBuilder<T> ConfigureConsoleLogs<T>(this IResourceBuilde
1818
// Enable ANSI Control Sequences for colors in Output Redirection
1919
context.EnvironmentVariables["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = "true";
2020

21-
// Enable Simple Console Logger Formatting with a UTC timestamp similar to RFC3339Nano that Docker generates
21+
// Enable Simple Console Logger Formatting
2222
context.EnvironmentVariables["LOGGING__CONSOLE__FORMATTERNAME"] = "simple";
23-
context.EnvironmentVariables["LOGGING__CONSOLE__FORMATTEROPTIONS__TIMESTAMPFORMAT"] = $"{KnownFormats.ConsoleLogsTimestampFormat} ";
2423
});
2524
}
2625
}

src/Shared/KnownFormats.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ namespace Aspire;
66
internal static class KnownFormats
77
{
88
/// <summary>
9-
/// Format is passed to apps as an env var to override logging's timestamp format.
10-
/// It is also used to parse logs when they're displayed in the dashboard's console logs UI.
9+
/// Internal timestamp format that is used to add the timestamp to a log line.
10+
/// Preserve second precision and timezone information.
1111
/// </summary>
12-
public const string ConsoleLogsTimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffffff";
12+
public const string ConsoleLogsTimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffffffK";
13+
14+
/// <summary>
15+
/// UI timestamp displayed on the console logs UI.
16+
/// </summary>
17+
public const string ConsoleLogsUITimestampFormat = "yyyy-MM-ddTHH:mm:ss";
1318
}

tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ public async Task ResourceLogsAreForwardedToHostLogging()
119119
// Category is derived from the application name and resource name
120120
// Logs sent at information level or lower are logged as information, otherwise they are logged as error
121121
Assert.Collection(hostLogs,
122-
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("1: 2000-12-29T20:59:59.0000000 Test trace message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
123-
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("2: 2000-12-29T20:59:59.0000000 Test debug message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
124-
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("3: 2000-12-29T20:59:59.0000000 Test information message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
125-
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("4: 2000-12-29T20:59:59.0000000 Test warning message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
126-
log => { Assert.Equal(LogLevel.Error, log.Level); Assert.Equal("5: 2000-12-29T20:59:59.0000000 Test error message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
127-
log => { Assert.Equal(LogLevel.Error, log.Level); Assert.Equal("6: 2000-12-29T20:59:59.0000000 Test critical message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); });
122+
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("1: 2000-12-29T20:59:59.0000000Z Test trace message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
123+
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("2: 2000-12-29T20:59:59.0000000Z Test debug message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
124+
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("3: 2000-12-29T20:59:59.0000000Z Test information message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
125+
log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("4: 2000-12-29T20:59:59.0000000Z Test warning message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
126+
log => { Assert.Equal(LogLevel.Error, log.Level); Assert.Equal("5: 2000-12-29T20:59:59.0000000Z Test error message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
127+
log => { Assert.Equal(LogLevel.Error, log.Level); Assert.Equal("6: 2000-12-29T20:59:59.0000000Z Test critical message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); });
128128
}
129129

130130
private sealed class CustomResource(string name) : Resource(name)

tests/Aspire.Hosting.Tests/Dcp/ApplicationExecutorTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ public async Task ResourceLogging_MultipleStreams_StreamedOverTime()
424424
await pipes.StandardOut.Writer.WriteAsync(Encoding.UTF8.GetBytes("2024-08-19T06:10:33.473275911Z Hello world" + Environment.NewLine));
425425
Assert.True(await moveNextTask);
426426
var logLine = watchLogsEnumerator.Current.Single();
427-
Assert.Equal("2024-08-19T06:10:33.4732759 Hello world", logLine.Content);
427+
Assert.Equal("2024-08-19T06:10:33.4732759Z Hello world", logLine.Content);
428428
Assert.Equal(1, logLine.LineNumber);
429429
Assert.False(logLine.IsErrorMessage);
430430

@@ -435,7 +435,7 @@ public async Task ResourceLogging_MultipleStreams_StreamedOverTime()
435435
await pipes.StandardErr.Writer.WriteAsync(Encoding.UTF8.GetBytes("2024-08-19T06:10:32.661Z Next" + Environment.NewLine));
436436
Assert.True(await moveNextTask);
437437
logLine = watchLogsEnumerator.Current.Single();
438-
Assert.Equal("2024-08-19T06:10:32.6610000 Next", logLine.Content);
438+
Assert.Equal("2024-08-19T06:10:32.6610000Z Next", logLine.Content);
439439
Assert.Equal(2, logLine.LineNumber);
440440
Assert.True(logLine.IsErrorMessage);
441441

tests/Aspire.Hosting.Tests/ProjectResourceTests.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,6 @@ public async Task AddProjectAddsEnvironmentVariablesAndServiceMetadata()
163163
{
164164
Assert.Equal("LOGGING__CONSOLE__FORMATTERNAME", env.Key);
165165
Assert.Equal("simple", env.Value);
166-
},
167-
env =>
168-
{
169-
Assert.Equal("LOGGING__CONSOLE__FORMATTEROPTIONS__TIMESTAMPFORMAT", env.Key);
170-
Assert.Equal("yyyy-MM-ddTHH:mm:ss.fffffff ", env.Value);
171166
});
172167
}
173168

tests/Aspire.Hosting.Tests/ResourceLoggerServiceTests.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ public async Task AddingResourceLoggerAnnotationAllowsLogging()
3131
// Wait for logs to be read
3232
var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15));
3333

34-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, world!", allLogs[0].Content);
34+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content);
3535
Assert.False(allLogs[0].IsErrorMessage);
3636

37-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, error!", allLogs[1].Content);
37+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, error!", allLogs[1].Content);
3838
Assert.True(allLogs[1].IsErrorMessage);
3939

4040
// New sub should get the previous logs
@@ -45,8 +45,8 @@ public async Task AddingResourceLoggerAnnotationAllowsLogging()
4545
allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15));
4646

4747
Assert.Equal(2, allLogs.Count);
48-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, world!", allLogs[0].Content);
49-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, error!", allLogs[1].Content);
48+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content);
49+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, error!", allLogs[1].Content);
5050

5151
await logsEnumerator1.DisposeAsync();
5252
await logsEnumerator2.DisposeAsync();
@@ -76,8 +76,8 @@ public async Task StreamingLogsCancelledAfterComplete()
7676
var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15));
7777

7878
Assert.Collection(allLogs,
79-
l => Assert.Equal("2000-12-29T20:59:59.0000000 Hello, world!", l.Content),
80-
l => Assert.Equal("2000-12-29T20:59:59.0000000 Hello, error!", l.Content));
79+
l => Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", l.Content),
80+
l => Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, error!", l.Content));
8181

8282
// New sub should not get new logs as the stream is completed
8383
logsLoop = ConsoleLoggingTestHelpers.WatchForLogsAsync(service, 100, testResource);
@@ -107,10 +107,10 @@ public async Task SecondSubscriberGetsBacklog()
107107
// Wait for logs to be read
108108
var allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15));
109109

110-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, world!", allLogs[0].Content);
110+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content);
111111
Assert.False(allLogs[0].IsErrorMessage);
112112

113-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, error!", allLogs[1].Content);
113+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, error!", allLogs[1].Content);
114114
Assert.True(allLogs[1].IsErrorMessage);
115115

116116
// New sub should get the previous logs (backlog)
@@ -121,8 +121,8 @@ public async Task SecondSubscriberGetsBacklog()
121121
allLogs = await logsLoop.WaitAsync(TimeSpan.FromSeconds(15));
122122

123123
Assert.Equal(2, allLogs.Count);
124-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, world!", allLogs[0].Content);
125-
Assert.Equal("2000-12-29T20:59:59.0000000 Hello, error!", allLogs[1].Content);
124+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, world!", allLogs[0].Content);
125+
Assert.Equal("2000-12-29T20:59:59.0000000Z Hello, error!", allLogs[1].Content);
126126

127127
// Clear the backlog and ensure new subs only get new logs
128128
service.ClearBacklog(testResource.Name);
@@ -136,7 +136,7 @@ public async Task SecondSubscriberGetsBacklog()
136136

137137
// The backlog should be cleared so only new logs are received
138138
Assert.Equal(1, allLogs.Count);
139-
Assert.Equal("2000-12-29T20:59:59.0000000 The third log", allLogs[0].Content);
139+
Assert.Equal("2000-12-29T20:59:59.0000000Z The third log", allLogs[0].Content);
140140
}
141141

142142
private sealed class TestResource(string name) : Resource(name)

0 commit comments

Comments
 (0)