Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix console logs page when linked from resource #5776

Merged
merged 6 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</MobilePageTitleToolbarSection>

<MainSection>
<LogViewer @ref="_logViewer"/>
<LogViewer @ref="LogViewer"/>
</MainSection>
</AspirePageContentLayout>
</div>
108 changes: 55 additions & 53 deletions src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,16 @@ private sealed class ConsoleLogsSubscription
[Parameter]
public string? ResourceName { get; set; }

private readonly TaskCompletionSource _logViewerReadyTcs = new();
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
private readonly CancellationTokenSource _resourceSubscriptionCancellation = new();
private readonly ConcurrentDictionary<string, ResourceViewModel> _resourceByName = new(StringComparers.ResourceName);
private ImmutableList<SelectViewModel<ResourceTypeDetails>>? _resources;
private Task? _resourceSubscriptionTask;
private ConsoleLogsSubscription? _consoleLogsSubscription;

// UI
public LogViewer LogViewer = null!;
private SelectViewModel<ResourceTypeDetails> _noSelection = null!;
private LogViewer _logViewer = null!;
private AspirePageContentLayout? _contentLayout;

// State
Expand Down Expand Up @@ -118,7 +119,7 @@ async Task TrackResourceSnapshotsAsync()
{
if (_resourceByName.TryGetValue(ResourceName, out var selectedResource))
{
SetSelectedResourceOption(selectedResource);
await SetSelectedResourceOption(selectedResource);
}
}
else
Expand All @@ -144,20 +145,22 @@ async Task TrackResourceSnapshotsAsync()
// we should mark it as selected
if (ResourceName is not null && PageViewModel.SelectedResource is null && changeType == ResourceViewModelChangeType.Upsert && string.Equals(ResourceName, resource.Name))
{
SetSelectedResourceOption(resource);
await SetSelectedResourceOption(resource);
}
}
}
});
}

void SetSelectedResourceOption(ResourceViewModel resource)
async Task SetSelectedResourceOption(ResourceViewModel resource)
{
Debug.Assert(_resources is not null);

PageViewModel.SelectedOption = _resources.Single(option => option.Id?.Type is not OtlpApplicationType.ResourceGrouping && string.Equals(ResourceName, option.Id?.InstanceId, StringComparison.Ordinal));
PageViewModel.SelectedResource = resource;

await this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: false);

Logger.LogDebug("Selected console resource from name {ResourceName}.", ResourceName);
loadingTcs.TrySetResult();
}
Expand Down Expand Up @@ -191,7 +194,10 @@ protected override async Task OnParametersSetAsync()
_consoleLogsSubscription = newConsoleLogsSubscription;
}

ClearLogs();
// Wait for the first render to complete so that the log viewer is available.
await _logViewerReadyTcs.Task;

LogViewer.ClearLogs();

if (newConsoleLogsSubscription is not null)
{
Expand All @@ -200,6 +206,14 @@ protected override async Task OnParametersSetAsync()
}
}

protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_logViewerReadyTcs.SetResult();
}
}

internal static ImmutableList<SelectViewModel<ResourceTypeDetails>> GetConsoleLogResourceSelectViewModels(
ConcurrentDictionary<string, ResourceViewModel> resourcesByName,
SelectViewModel<ResourceTypeDetails> noSelectionViewModel,
Expand Down Expand Up @@ -271,71 +285,59 @@ string GetDisplayText()

private void UpdateResourcesList() => _resources = GetConsoleLogResourceSelectViewModels(_resourceByName, _noSelection, Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsUnknownState)]);

private void ClearLogs()
{
_logViewer?.ClearLogs();
}

private void LoadLogs(ConsoleLogsSubscription newConsoleLogsSubscription)
{
if (_logViewer is null)
var consoleLogsTask = Task.Run(async () =>
{
PageViewModel.Status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsInitializingLogViewer)];
}
else
{
var consoleLogsTask = Task.Run(async () =>
{
newConsoleLogsSubscription.CancellationToken.ThrowIfCancellationRequested();
newConsoleLogsSubscription.CancellationToken.ThrowIfCancellationRequested();

Logger.LogDebug("Subscribing to console logs for resource {ResourceName}.", newConsoleLogsSubscription.Name);
Logger.LogDebug("Subscribing to console logs for resource {ResourceName}.", newConsoleLogsSubscription.Name);

var subscription = DashboardClient.SubscribeConsoleLogs(newConsoleLogsSubscription.Name, newConsoleLogsSubscription.CancellationToken);
var subscription = DashboardClient.SubscribeConsoleLogs(newConsoleLogsSubscription.Name, newConsoleLogsSubscription.CancellationToken);

PageViewModel.Status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsWatchingLogs)];
await InvokeAsync(StateHasChanged);
PageViewModel.Status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsWatchingLogs)];
await InvokeAsync(StateHasChanged);

try
try
{
var logParser = new LogParser();
await foreach (var batch in subscription.ConfigureAwait(true))
{
var logParser = new LogParser();
await foreach (var batch in subscription.ConfigureAwait(true))
if (batch.Count is 0)
{
if (batch.Count is 0)
{
continue;
}
continue;
}

foreach (var (lineNumber, content, isErrorOutput) in batch)
foreach (var (lineNumber, content, isErrorOutput) in batch)
{
// Set the base line number using the reported line number of the first log line.
if (LogViewer.LogEntries.EntriesCount == 0)
{
// Set the base line number using the reported line number of the first log line.
if (_logViewer.LogEntries.EntriesCount == 0)
{
_logViewer.LogEntries.BaseLineNumber = lineNumber;
}

var logEntry = logParser.CreateLogEntry(content, isErrorOutput);
_logViewer.LogEntries.InsertSorted(logEntry);
LogViewer.LogEntries.BaseLineNumber = lineNumber;
}

await _logViewer.LogsAddedAsync();
var logEntry = logParser.CreateLogEntry(content, isErrorOutput);
LogViewer.LogEntries.InsertSorted(logEntry);
}

await LogViewer.LogsAddedAsync();
}
finally
{
Logger.LogDebug("Finished watching logs for resource {ResourceName}.", newConsoleLogsSubscription.Name);
}
finally
{
Logger.LogDebug("Finished watching logs for resource {ResourceName}.", newConsoleLogsSubscription.Name);

// If the subscription is being canceled then a new one could be starting.
// Don't set the status when finishing because overwrite the status from the new subscription.
if (!newConsoleLogsSubscription.CancellationToken.IsCancellationRequested)
{
PageViewModel.Status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsFinishedWatchingLogs)];
await InvokeAsync(StateHasChanged);
}
// If the subscription is being canceled then a new one could be starting.
// Don't set the status when finishing because overwrite the status from the new subscription.
if (!newConsoleLogsSubscription.CancellationToken.IsCancellationRequested)
{
PageViewModel.Status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsFinishedWatchingLogs)];
await InvokeAsync(StateHasChanged);
}
});
}
});

newConsoleLogsSubscription.SubscriptionTask = consoleLogsTask;
}
newConsoleLogsSubscription.SubscriptionTask = consoleLogsTask;
}

private async Task HandleSelectedOptionChangedAsync()
Expand Down Expand Up @@ -393,7 +395,7 @@ public async ValueTask DisposeAsync()

await StopAndClearConsoleLogsSubscriptionAsync();

if (_logViewer is { } logViewer)
if (LogViewer is { } logViewer)
{
await logViewer.DisposeAsync();
}
Expand Down
46 changes: 46 additions & 0 deletions tests/Aspire.Dashboard.Components.Tests/Pages/ConsoleLogsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,52 @@ public void ResourceName_MultiRender_SubscribeConsoleLogsOnce()
Assert.Equal("test-resource", Assert.Single(subscribedResourceNames));
}

[Fact]
public void ResourceName_ViaUrlAndResourceLoaded_LogViewerUpdated()
{
// Arrange
var testResource = CreateResourceViewModel("test-resource", KnownResourceState.Running);
var subscribedResourceNames = new List<string>();
var consoleLogsChannel = Channel.CreateUnbounded<IReadOnlyList<ResourceLogLine>>();
var resourceChannel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
var dashboardClient = new TestDashboardClient(
isEnabled: true,
consoleLogsChannelProvider: name =>
{
subscribedResourceNames.Add(name);
return consoleLogsChannel;
},
resourceChannelProvider: () => resourceChannel,
initialResources: [testResource]);

SetupConsoleLogsServices(dashboardClient);

var dimensionManager = Services.GetRequiredService<DimensionManager>();
var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
dimensionManager.InvokeOnViewportInformationChanged(viewport);

// Act
var cut = RenderComponent<Components.Pages.ConsoleLogs>(builder =>
{
builder.Add(p => p.ResourceName, "test-resource");
builder.Add(p => p.ViewportInformation, viewport);
});

var instance = cut.Instance;
var logger = Services.GetRequiredService<ILogger<ConsoleLogsTests>>();
var loc = Services.GetRequiredService<IStringLocalizer<Resources.ConsoleLogs>>();

// Assert
logger.LogInformation("Resource and subscription should be set immediately on first render.");
Assert.Equal(testResource, instance.PageViewModel.SelectedResource);
Assert.Equal(loc[nameof(Resources.ConsoleLogs.ConsoleLogsWatchingLogs)], instance.PageViewModel.Status);
Assert.Equal("test-resource", Assert.Single(subscribedResourceNames));

logger.LogInformation("Log results are added to log viewer.");
consoleLogsChannel.Writer.TryWrite([new ResourceLogLine(1, "Hello world", IsErrorMessage: false)]);
cut.WaitForState(() => instance.LogViewer.LogEntries.EntriesCount > 0);
}

private void SetupConsoleLogsServices(TestDashboardClient? dashboardClient = null)
{
var version = typeof(FluentMain).Assembly.GetName().Version!;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Aspire.Dashboard.Model;
Expand All @@ -11,6 +12,7 @@ public class TestDashboardClient : IDashboardClient
{
private readonly Func<string, Channel<IReadOnlyList<ResourceLogLine>>>? _consoleLogsChannelProvider;
private readonly Func<Channel<IReadOnlyList<ResourceViewModelChange>>>? _resourceChannelProvider;
private readonly IList<ResourceViewModel>? _initialResources;

public bool IsEnabled { get; }
public Task WhenConnected { get; } = Task.CompletedTask;
Expand All @@ -19,11 +21,13 @@ public class TestDashboardClient : IDashboardClient
public TestDashboardClient(
bool? isEnabled = false,
Func<string, Channel<IReadOnlyList<ResourceLogLine>>>? consoleLogsChannelProvider = null,
Func<Channel<IReadOnlyList<ResourceViewModelChange>>>? resourceChannelProvider = null)
Func<Channel<IReadOnlyList<ResourceViewModelChange>>>? resourceChannelProvider = null,
IList<ResourceViewModel>? initialResources = null)
{
IsEnabled = isEnabled ?? false;
_consoleLogsChannelProvider = consoleLogsChannelProvider;
_resourceChannelProvider = resourceChannelProvider;
_initialResources = initialResources;
}

public ValueTask DisposeAsync()
Expand Down Expand Up @@ -60,7 +64,7 @@ public Task<ResourceViewModelSubscription> SubscribeResourcesAsync(CancellationT

var channel = _resourceChannelProvider();

return Task.FromResult(new ResourceViewModelSubscription([], BuildSubscription(channel, cancellationToken)));
return Task.FromResult(new ResourceViewModelSubscription(_initialResources?.ToImmutableArray() ?? [], BuildSubscription(channel, cancellationToken)));

async static IAsyncEnumerable<IReadOnlyList<ResourceViewModelChange>> BuildSubscription(Channel<IReadOnlyList<ResourceViewModelChange>> channel, [EnumeratorCancellation] CancellationToken cancellationToken)
{
Expand Down