Skip to content

Commit

Permalink
Display new error logs count to projects page (#545)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Oct 28, 2023
1 parent a054e4a commit 4032b57
Show file tree
Hide file tree
Showing 21 changed files with 538 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@namespace Aspire.Dashboard.Components

@if (UnviewedCount > 0)
{
<div @onclick="OnClick" class="resource-error-badge">
<FluentBadge title="@($"{UnviewedCount} error logs")" Appearance="Appearance.Accent" Circular="true" Fill="error" slot="end">
@((UnviewedCount > 9) ? "9+" : UnviewedCount.ToString())
</FluentBadge>
</div>
}

@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }

[Parameter]
public int UnviewedCount { get; set; }
}
7 changes: 6 additions & 1 deletion src/Aspire.Dashboard/Components/Pages/Containers.razor
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
<TemplateColumn Title="Container ID">
<GridValue HighlightText="@filter" Value="@context.ContainerId" />
</TemplateColumn>
<PropertyColumn Property="@(e => e.State)" Sortable="true" Tooltip="true" />
<TemplateColumn Title="State" Sortable="true" SortBy="@stateSort">
<div class="resource-state-container">
@context.State
<UnreadLogErrorsBadge UnviewedCount="@GetUnviewedErrorCount(context)" OnClick="@(() => ViewErrorStructuredLogs(context))" />
</div>
</TemplateColumn>
<PropertyColumn Property="@(c => c.CreationTimeStamp)" Title="Start Time" Sortable="true" Tooltip="true" />
<TemplateColumn Title="Container Image" Sortable="true" SortBy="@imageSort" Tooltip="true" TooltipText="(c) => c.Image">
<FluentHighlighter HighlightedText="@filter" Text="@context.Image" />
Expand Down
9 changes: 7 additions & 2 deletions src/Aspire.Dashboard/Components/Pages/Executables.razor
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
slot="end" />
</FluentToolbar>
<div class="datagrid-overflow-area" tabindex="-1">
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 1fr 2fr 1fr 4fr 4fr 4fr 2fr 1fr 1fr">
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 2fr 2fr 1fr 3fr 3fr 3fr 2fr 1fr 1fr">
<ChildContent>
<TemplateColumn Title="Name" Sortable="true" SortBy="@nameSort" Tooltip="true" TooltipText="(e) => e.Name">
<FluentHighlighter HighlightedText="@filter" Text="@context.Name" />
</TemplateColumn>
<PropertyColumn Property="@(e => e.State)" Sortable="true" Tooltip="true" />
<TemplateColumn Title="State" Sortable="true" SortBy="@stateSort">
<div class="resource-state-container">
@context.State
<UnreadLogErrorsBadge UnviewedCount="@GetUnviewedErrorCount(context)" OnClick="@(() => ViewErrorStructuredLogs(context))" />
</div>
</TemplateColumn>
<PropertyColumn Property="@(c => c.CreationTimeStamp)" Title="Start Time" Sortable="true" Tooltip="true" />
<PropertyColumn Property="@(c => c.ProcessId)" Title="Process Id" Sortable="true" Tooltip="true" />
<TemplateColumn Title="Path" Sortable="true" SortBy="@executablePathSort" Tooltip="true" TooltipText="(e) => e.ExecutablePath">
Expand Down
29 changes: 8 additions & 21 deletions src/Aspire.Dashboard/Components/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@
slot="end" />
</FluentToolbar>
<div class="datagrid-overflow-area" tabindex="-1">
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 1fr 2fr 1fr 4fr 2fr 1fr 1fr" >
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 1.2fr 2fr 1fr 4fr 2fr 1fr 1fr" >
<ChildContent>
<TemplateColumn Title="Name" Sortable="true" SortBy="@nameSort" Tooltip="true" TooltipText="(p) => p.Name">
<FluentHighlighter HighlightedText="@filter" Text="@context.Name" />
</TemplateColumn>
<PropertyColumn Property="@(e => e.State)" Sortable="true" Tooltip="true" />
<TemplateColumn Title="State" Sortable="true" SortBy="@stateSort">
<div class="resource-state-container">
@context.State
<UnreadLogErrorsBadge UnviewedCount="@GetUnviewedErrorCount(context)" OnClick="@(() => ViewErrorStructuredLogs(context))" />
</div>
</TemplateColumn>
<PropertyColumn Property="@(c => c.CreationTimeStamp)" Title="Start Time" Sortable="true" Tooltip="true" />
<PropertyColumn Property="@(c => c.ProcessId)" Title="Process Id" Sortable="true" Tooltip="true" />
<TemplateColumn Title="Source Location" Sortable="true" SortBy="@projectPathSort" Tooltip="true" TooltipText="(p) => p.ProjectPath">
<TemplateColumn Title="Source Location" Sortable="true" SortBy="@_projectPathSort" Tooltip="true" TooltipText="(p) => p.ProjectPath">
<FluentHighlighter HighlightedText="@filter" Text="@context.ProjectPath" />
</TemplateColumn>
<TemplateColumn Title="Endpoints" Sortable="false">
Expand Down Expand Up @@ -67,21 +72,3 @@
</FluentDataGrid>
</div>
</div>

@code
{
protected override ValueTask<List<ProjectViewModel>> GetResources(IDashboardViewModelService dashboardViewModelService)
=> dashboardViewModelService.GetProjectsAsync();

protected override IAsyncEnumerable<ResourceChanged<ProjectViewModel>> WatchResources(
IDashboardViewModelService dashboardViewModelService,
IEnumerable<NamespacedName> initialList,
CancellationToken cancellationToken)
=> dashboardViewModelService.WatchProjectsAsync(initialList, cancellationToken);

protected override bool Filter(ProjectViewModel resource)
=> resource.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase)
|| resource.ProjectPath.Contains(filter, StringComparison.CurrentCultureIgnoreCase);

private GridSort<ProjectViewModel> projectPathSort = GridSort<ProjectViewModel>.ByAscending(p => p.ProjectPath);
}
25 changes: 25 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Index.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
using Microsoft.Fast.Components.FluentUI;

namespace Aspire.Dashboard.Components.Pages;

public partial class Index : ResourcesListBase<ProjectViewModel>
{
protected override ValueTask<List<ProjectViewModel>> GetResources(IDashboardViewModelService dashboardViewModelService)
=> dashboardViewModelService.GetProjectsAsync();

protected override IAsyncEnumerable<ResourceChanged<ProjectViewModel>> WatchResources(
IDashboardViewModelService dashboardViewModelService,
IEnumerable<NamespacedName> initialList,
CancellationToken cancellationToken)
=> dashboardViewModelService.WatchProjectsAsync(initialList, cancellationToken);

protected override bool Filter(ProjectViewModel resource)
=> resource.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase)
|| resource.ProjectPath.Contains(filter, StringComparison.CurrentCultureIgnoreCase);

private readonly GridSort<ProjectViewModel> _projectPathSort = GridSort<ProjectViewModel>.ByAscending(p => p.ProjectPath);
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private void UpdateSubscription()
if (_metricsSubscription is null || _metricsSubscription.ApplicationId != _selectedApplication.Id)
{
_metricsSubscription?.Dispose();
_metricsSubscription = TelemetryRepository.OnNewMetrics(_selectedApplication.Id, async () =>
_metricsSubscription = TelemetryRepository.OnNewMetrics(_selectedApplication.Id, SubscriptionType.Read, async () =>
{
var selectedApplicationId = _selectedApplication.Id;
if (!string.IsNullOrEmpty(selectedApplicationId))
Expand Down
60 changes: 57 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/ResourcesListBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,32 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Fast.Components.FluentUI;

namespace Aspire.Dashboard.Components.Pages;

public abstract class ResourcesListBase<TResource> : ComponentBase
public abstract class ResourcesListBase<TResource> : ComponentBase, IDisposable
where TResource : ResourceViewModel
{
// Ideally we'd be pulling this from Aspire.Hosting.Dcp.Model.ExecutableStates,
// but unfortunately the reference goes the other way
protected const string FinishedState = "Finished";

private Subscription? _logsSubscription;
private Dictionary<OtlpApplication, int>? _applicationUnviewedErrorCounts;

[Inject]
public required IDashboardViewModelService DashboardViewModelService { get; init; }
[Inject]
public required EnvironmentVariablesDialogService EnvironmentVariablesDialogService { get; init; }
[Inject]
public required TelemetryRepository TelemetryRepository { get; init; }
[Inject]
public required NavigationManager NavigationManager { get; set; }

protected abstract ValueTask<List<TResource>> GetResources(IDashboardViewModelService dashboardViewModelService);
protected abstract IAsyncEnumerable<ResourceChanged<TResource>> WatchResources(
Expand All @@ -35,9 +44,12 @@ protected abstract IAsyncEnumerable<ResourceChanged<TResource>> WatchResources(
protected IQueryable<TResource>? FilteredResources => _resourcesMap.Values.Where(Filter).OrderBy(e => e.Name).AsQueryable();

protected GridSort<TResource> nameSort = GridSort<TResource>.ByAscending(p => p.Name);
protected GridSort<TResource> stateSort = GridSort<TResource>.ByAscending(p => p.State);

protected override async Task OnInitializedAsync()
{
_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();

var resources = await GetResources(DashboardViewModelService);
foreach (var resource in resources)
{
Expand All @@ -52,6 +64,33 @@ protected override async Task OnInitializedAsync()
await OnResourceListChanged(resourceChanged.ObjectChangeType, resourceChanged.Resource);
}
});

_logsSubscription = TelemetryRepository.OnNewLogs(null, SubscriptionType.Other, async () =>
{
_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();
await InvokeAsync(StateHasChanged);
});
}

protected int GetUnviewedErrorCount(TResource resource)
{
if (_applicationUnviewedErrorCounts is null)
{
return 0;
}

var application = TelemetryRepository.GetApplication(resource.Uid);
if (application is null)
{
return 0;
}

if (!_applicationUnviewedErrorCounts.TryGetValue(application, out var count))
{
return 0;
}

return count;
}

protected async Task ShowEnvironmentVariables(TResource resource)
Expand Down Expand Up @@ -86,10 +125,20 @@ private async Task OnResourceListChanged(ObjectChangeType objectChangeType, TRes
await InvokeAsync(StateHasChanged);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_watchTaskCancellationTokenSource.Cancel();
_watchTaskCancellationTokenSource.Dispose();
_logsSubscription?.Dispose();
}
}

public void Dispose()
{
_watchTaskCancellationTokenSource.Cancel();
_watchTaskCancellationTokenSource.Dispose();
Dispose(true);
GC.SuppressFinalize(this);
}

protected void HandleFilter(ChangeEventArgs args)
Expand All @@ -104,4 +153,9 @@ protected void HandleClear(string? value)
{
filter = value ?? string.Empty;
}

protected void ViewErrorStructuredLogs(TResource resource)
{
NavigationManager.NavigateTo($"/StructuredLogs/{resource.Uid}?level=error");
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<span slot="end">Level:</span>
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<FluentSelect TOption="SelectViewModel<LogLevel>"
<FluentSelect TOption="SelectViewModel<LogLevel?>"
Items="@_logLevels"
OptionText="@(c => c.Name)"
@bind-SelectedOption="_selectedLogLevel"
Expand Down
63 changes: 50 additions & 13 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public partial class StructuredLogs

private TotalItemsFooter _totalItemsFooter = default!;
private List<SelectViewModel<string>> _applications = default!;
private List<SelectViewModel<LogLevel>> _logLevels = default!;
private List<SelectViewModel<LogLevel?>> _logLevels = default!;
private SelectViewModel<string> _selectedApplication = s_allApplication;
private SelectViewModel<LogLevel> _selectedLogLevel = default!;
private SelectViewModel<LogLevel?> _selectedLogLevel = default!;
private Subscription? _applicationsSubscription;
private Subscription? _logsSubscription;
private bool _applicationChanged;
Expand All @@ -48,6 +48,10 @@ public partial class StructuredLogs
[SupplyParameterFromQuery]
public string? SpanId { get; set; }

[Parameter]
[SupplyParameterFromQuery(Name = "level")]
public string? LogLevelText { get; set; }

private ValueTask<GridItemsProviderResult<OtlpLogEntry>> GetData(GridItemsProviderRequest<OtlpLogEntry> request)
{
ViewModel.StartIndex = request.StartIndex;
Expand All @@ -59,6 +63,8 @@ private ValueTask<GridItemsProviderResult<OtlpLogEntry>> GetData(GridItemsProvid
// The workaround is to put the count inside a control and explicitly update and refresh the control.
_totalItemsFooter.SetTotalItemCount(logs.TotalItemCount);

TelemetryRepository.MarkViewedErrorLogs(ViewModel.ApplicationServiceId);

return ValueTask.FromResult(GridItemsProviderResult.From(logs.Items, logs.TotalItemCount));
}

Expand All @@ -73,15 +79,15 @@ protected override Task OnInitializedAsync()
ViewModel.AddFilter(new LogFilter { Field = "SpanId", Condition = FilterCondition.Equals, Value = SpanId });
}

_logLevels = new List<SelectViewModel<LogLevel>>
_logLevels = new List<SelectViewModel<LogLevel?>>
{
new SelectViewModel<LogLevel> { Id = LogLevel.Trace, Name = "(All)" },
new SelectViewModel<LogLevel> { Id = LogLevel.Trace, Name = "Trace" },
new SelectViewModel<LogLevel> { Id = LogLevel.Debug, Name = "Debug" },
new SelectViewModel<LogLevel> { Id = LogLevel.Information, Name = "Information" },
new SelectViewModel<LogLevel> { Id = LogLevel.Warning, Name = "Warning" },
new SelectViewModel<LogLevel> { Id = LogLevel.Error, Name = "Error" },
new SelectViewModel<LogLevel> { Id = LogLevel.Critical, Name = "Critical" },
new SelectViewModel<LogLevel?> { Id = null, Name = "(All)" },
new SelectViewModel<LogLevel?> { Id = LogLevel.Trace, Name = "Trace" },
new SelectViewModel<LogLevel?> { Id = LogLevel.Debug, Name = "Debug" },
new SelectViewModel<LogLevel?> { Id = LogLevel.Information, Name = "Information" },
new SelectViewModel<LogLevel?> { Id = LogLevel.Warning, Name = "Warning" },
new SelectViewModel<LogLevel?> { Id = LogLevel.Error, Name = "Error" },
new SelectViewModel<LogLevel?> { Id = LogLevel.Critical, Name = "Critical" },
};
_selectedLogLevel = _logLevels[0];

Expand All @@ -99,6 +105,17 @@ protected override void OnParametersSet()
{
_selectedApplication = _applications.SingleOrDefault(e => e.Id == ApplicationInstanceId) ?? s_allApplication;
ViewModel.ApplicationServiceId = _selectedApplication.Id;

if (LogLevelText != null && Enum.TryParse<LogLevel>(LogLevelText, ignoreCase: true, out var logLevel))
{
_selectedLogLevel = _logLevels.SingleOrDefault(e => e.Id == logLevel) ?? _logLevels[0];
}
else
{
_selectedLogLevel = _logLevels[0];
}
ViewModel.LogLevel = _selectedLogLevel.Id;

UpdateSubscription();
}

Expand All @@ -110,15 +127,15 @@ private void UpdateApplications()

private Task HandleSelectedApplicationChangedAsync()
{
NavigationManager.NavigateTo($"/StructuredLogs/{_selectedApplication.Id}");
NavigateTo(_selectedApplication.Id, _selectedLogLevel.Id);
_applicationChanged = true;

return Task.CompletedTask;
}

private Task HandleSelectedLogLevelChangedAsync()
{
ViewModel.LogLevel = _selectedLogLevel.Id;
NavigateTo(_selectedApplication.Id, _selectedLogLevel.Id);
_applicationChanged = true;

return Task.CompletedTask;
Expand All @@ -130,7 +147,7 @@ private void UpdateSubscription()
if (_logsSubscription is null || _logsSubscription.ApplicationId != _selectedApplication.Id)
{
_logsSubscription?.Dispose();
_logsSubscription = TelemetryRepository.OnNewLogs(_selectedApplication.Id, async () =>
_logsSubscription = TelemetryRepository.OnNewLogs(_selectedApplication.Id, SubscriptionType.Read, async () =>
{
ViewModel.ClearData();
await InvokeAsync(StateHasChanged);
Expand Down Expand Up @@ -221,6 +238,26 @@ private void HandleClear(string value)
StateHasChanged();
}

private void NavigateTo(string? applicationId, LogLevel? level)
{
string url;
if (applicationId != null)
{
url = $"/StructuredLogs/{applicationId}";
}
else
{
url = $"/StructuredLogs";
}

if (level != null)
{
url += $"?level={level.Value.ToString().ToLowerInvariant()}";
}

NavigationManager.NavigateTo(url);
}

private static string GetRowClass(OtlpLogEntry entry)
{
return $"log-row-{entry.Severity.ToString().ToLowerInvariant()}";
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private void UpdateDetailViewData()
if (_tracesSubscription is null || _tracesSubscription.ApplicationId != _trace.FirstSpan.Source.InstanceId)
{
_tracesSubscription?.Dispose();
_tracesSubscription = TelemetryRepository.OnNewTraces(_trace.FirstSpan.Source.InstanceId, () => InvokeAsync(() =>
_tracesSubscription = TelemetryRepository.OnNewTraces(_trace.FirstSpan.Source.InstanceId, SubscriptionType.Read, () => InvokeAsync(() =>
{
UpdateDetailViewData();
StateHasChanged();
Expand Down
Loading

0 comments on commit 4032b57

Please sign in to comment.