Skip to content

Commit 4032b57

Browse files
authored
Display new error logs count to projects page (#545)
1 parent a054e4a commit 4032b57

21 files changed

+538
-60
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@namespace Aspire.Dashboard.Components
2+
3+
@if (UnviewedCount > 0)
4+
{
5+
<div @onclick="OnClick" class="resource-error-badge">
6+
<FluentBadge title="@($"{UnviewedCount} error logs")" Appearance="Appearance.Accent" Circular="true" Fill="error" slot="end">
7+
@((UnviewedCount > 9) ? "9+" : UnviewedCount.ToString())
8+
</FluentBadge>
9+
</div>
10+
}
11+
12+
@code {
13+
[Parameter]
14+
public EventCallback<MouseEventArgs> OnClick { get; set; }
15+
16+
[Parameter]
17+
public int UnviewedCount { get; set; }
18+
}

src/Aspire.Dashboard/Components/Pages/Containers.razor

+6-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
<TemplateColumn Title="Container ID">
2626
<GridValue HighlightText="@filter" Value="@context.ContainerId" />
2727
</TemplateColumn>
28-
<PropertyColumn Property="@(e => e.State)" Sortable="true" Tooltip="true" />
28+
<TemplateColumn Title="State" Sortable="true" SortBy="@stateSort">
29+
<div class="resource-state-container">
30+
@context.State
31+
<UnreadLogErrorsBadge UnviewedCount="@GetUnviewedErrorCount(context)" OnClick="@(() => ViewErrorStructuredLogs(context))" />
32+
</div>
33+
</TemplateColumn>
2934
<PropertyColumn Property="@(c => c.CreationTimeStamp)" Title="Start Time" Sortable="true" Tooltip="true" />
3035
<TemplateColumn Title="Container Image" Sortable="true" SortBy="@imageSort" Tooltip="true" TooltipText="(c) => c.Image">
3136
<FluentHighlighter HighlightedText="@filter" Text="@context.Image" />

src/Aspire.Dashboard/Components/Pages/Executables.razor

+7-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717
slot="end" />
1818
</FluentToolbar>
1919
<div class="datagrid-overflow-area" tabindex="-1">
20-
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 1fr 2fr 1fr 4fr 4fr 4fr 2fr 1fr 1fr">
20+
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 2fr 2fr 1fr 3fr 3fr 3fr 2fr 1fr 1fr">
2121
<ChildContent>
2222
<TemplateColumn Title="Name" Sortable="true" SortBy="@nameSort" Tooltip="true" TooltipText="(e) => e.Name">
2323
<FluentHighlighter HighlightedText="@filter" Text="@context.Name" />
2424
</TemplateColumn>
25-
<PropertyColumn Property="@(e => e.State)" Sortable="true" Tooltip="true" />
25+
<TemplateColumn Title="State" Sortable="true" SortBy="@stateSort">
26+
<div class="resource-state-container">
27+
@context.State
28+
<UnreadLogErrorsBadge UnviewedCount="@GetUnviewedErrorCount(context)" OnClick="@(() => ViewErrorStructuredLogs(context))" />
29+
</div>
30+
</TemplateColumn>
2631
<PropertyColumn Property="@(c => c.CreationTimeStamp)" Title="Start Time" Sortable="true" Tooltip="true" />
2732
<PropertyColumn Property="@(c => c.ProcessId)" Title="Process Id" Sortable="true" Tooltip="true" />
2833
<TemplateColumn Title="Path" Sortable="true" SortBy="@executablePathSort" Tooltip="true" TooltipText="(e) => e.ExecutablePath">

src/Aspire.Dashboard/Components/Pages/Index.razor

+8-21
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
slot="end" />
1818
</FluentToolbar>
1919
<div class="datagrid-overflow-area" tabindex="-1">
20-
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 1fr 2fr 1fr 4fr 2fr 1fr 1fr" >
20+
<FluentDataGrid Items="@FilteredResources" ResizableColumns="true" GridTemplateColumns="2fr 1.2fr 2fr 1fr 4fr 2fr 1fr 1fr" >
2121
<ChildContent>
2222
<TemplateColumn Title="Name" Sortable="true" SortBy="@nameSort" Tooltip="true" TooltipText="(p) => p.Name">
2323
<FluentHighlighter HighlightedText="@filter" Text="@context.Name" />
2424
</TemplateColumn>
25-
<PropertyColumn Property="@(e => e.State)" Sortable="true" Tooltip="true" />
25+
<TemplateColumn Title="State" Sortable="true" SortBy="@stateSort">
26+
<div class="resource-state-container">
27+
@context.State
28+
<UnreadLogErrorsBadge UnviewedCount="@GetUnviewedErrorCount(context)" OnClick="@(() => ViewErrorStructuredLogs(context))" />
29+
</div>
30+
</TemplateColumn>
2631
<PropertyColumn Property="@(c => c.CreationTimeStamp)" Title="Start Time" Sortable="true" Tooltip="true" />
2732
<PropertyColumn Property="@(c => c.ProcessId)" Title="Process Id" Sortable="true" Tooltip="true" />
28-
<TemplateColumn Title="Source Location" Sortable="true" SortBy="@projectPathSort" Tooltip="true" TooltipText="(p) => p.ProjectPath">
33+
<TemplateColumn Title="Source Location" Sortable="true" SortBy="@_projectPathSort" Tooltip="true" TooltipText="(p) => p.ProjectPath">
2934
<FluentHighlighter HighlightedText="@filter" Text="@context.ProjectPath" />
3035
</TemplateColumn>
3136
<TemplateColumn Title="Endpoints" Sortable="false">
@@ -67,21 +72,3 @@
6772
</FluentDataGrid>
6873
</div>
6974
</div>
70-
71-
@code
72-
{
73-
protected override ValueTask<List<ProjectViewModel>> GetResources(IDashboardViewModelService dashboardViewModelService)
74-
=> dashboardViewModelService.GetProjectsAsync();
75-
76-
protected override IAsyncEnumerable<ResourceChanged<ProjectViewModel>> WatchResources(
77-
IDashboardViewModelService dashboardViewModelService,
78-
IEnumerable<NamespacedName> initialList,
79-
CancellationToken cancellationToken)
80-
=> dashboardViewModelService.WatchProjectsAsync(initialList, cancellationToken);
81-
82-
protected override bool Filter(ProjectViewModel resource)
83-
=> resource.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase)
84-
|| resource.ProjectPath.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
85-
86-
private GridSort<ProjectViewModel> projectPathSort = GridSort<ProjectViewModel>.ByAscending(p => p.ProjectPath);
87-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Dashboard.Model;
5+
using Microsoft.Fast.Components.FluentUI;
6+
7+
namespace Aspire.Dashboard.Components.Pages;
8+
9+
public partial class Index : ResourcesListBase<ProjectViewModel>
10+
{
11+
protected override ValueTask<List<ProjectViewModel>> GetResources(IDashboardViewModelService dashboardViewModelService)
12+
=> dashboardViewModelService.GetProjectsAsync();
13+
14+
protected override IAsyncEnumerable<ResourceChanged<ProjectViewModel>> WatchResources(
15+
IDashboardViewModelService dashboardViewModelService,
16+
IEnumerable<NamespacedName> initialList,
17+
CancellationToken cancellationToken)
18+
=> dashboardViewModelService.WatchProjectsAsync(initialList, cancellationToken);
19+
20+
protected override bool Filter(ProjectViewModel resource)
21+
=> resource.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase)
22+
|| resource.ProjectPath.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
23+
24+
private readonly GridSort<ProjectViewModel> _projectPathSort = GridSort<ProjectViewModel>.ByAscending(p => p.ProjectPath);
25+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ private void UpdateSubscription()
193193
if (_metricsSubscription is null || _metricsSubscription.ApplicationId != _selectedApplication.Id)
194194
{
195195
_metricsSubscription?.Dispose();
196-
_metricsSubscription = TelemetryRepository.OnNewMetrics(_selectedApplication.Id, async () =>
196+
_metricsSubscription = TelemetryRepository.OnNewMetrics(_selectedApplication.Id, SubscriptionType.Read, async () =>
197197
{
198198
var selectedApplicationId = _selectedApplication.Id;
199199
if (!string.IsNullOrEmpty(selectedApplicationId))

src/Aspire.Dashboard/Components/Pages/ResourcesListBase.cs

+57-3
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,32 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Aspire.Dashboard.Model;
5+
using Aspire.Dashboard.Otlp.Model;
6+
using Aspire.Dashboard.Otlp.Storage;
57
using Aspire.Dashboard.Services;
68
using Microsoft.AspNetCore.Components;
79
using Microsoft.Fast.Components.FluentUI;
810

911
namespace Aspire.Dashboard.Components.Pages;
1012

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

20+
private Subscription? _logsSubscription;
21+
private Dictionary<OtlpApplication, int>? _applicationUnviewedErrorCounts;
22+
1823
[Inject]
1924
public required IDashboardViewModelService DashboardViewModelService { get; init; }
2025
[Inject]
2126
public required EnvironmentVariablesDialogService EnvironmentVariablesDialogService { get; init; }
27+
[Inject]
28+
public required TelemetryRepository TelemetryRepository { get; init; }
29+
[Inject]
30+
public required NavigationManager NavigationManager { get; set; }
2231

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

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

3949
protected override async Task OnInitializedAsync()
4050
{
51+
_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();
52+
4153
var resources = await GetResources(DashboardViewModelService);
4254
foreach (var resource in resources)
4355
{
@@ -52,6 +64,33 @@ protected override async Task OnInitializedAsync()
5264
await OnResourceListChanged(resourceChanged.ObjectChangeType, resourceChanged.Resource);
5365
}
5466
});
67+
68+
_logsSubscription = TelemetryRepository.OnNewLogs(null, SubscriptionType.Other, async () =>
69+
{
70+
_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();
71+
await InvokeAsync(StateHasChanged);
72+
});
73+
}
74+
75+
protected int GetUnviewedErrorCount(TResource resource)
76+
{
77+
if (_applicationUnviewedErrorCounts is null)
78+
{
79+
return 0;
80+
}
81+
82+
var application = TelemetryRepository.GetApplication(resource.Uid);
83+
if (application is null)
84+
{
85+
return 0;
86+
}
87+
88+
if (!_applicationUnviewedErrorCounts.TryGetValue(application, out var count))
89+
{
90+
return 0;
91+
}
92+
93+
return count;
5594
}
5695

5796
protected async Task ShowEnvironmentVariables(TResource resource)
@@ -86,10 +125,20 @@ private async Task OnResourceListChanged(ObjectChangeType objectChangeType, TRes
86125
await InvokeAsync(StateHasChanged);
87126
}
88127

128+
protected virtual void Dispose(bool disposing)
129+
{
130+
if (disposing)
131+
{
132+
_watchTaskCancellationTokenSource.Cancel();
133+
_watchTaskCancellationTokenSource.Dispose();
134+
_logsSubscription?.Dispose();
135+
}
136+
}
137+
89138
public void Dispose()
90139
{
91-
_watchTaskCancellationTokenSource.Cancel();
92-
_watchTaskCancellationTokenSource.Dispose();
140+
Dispose(true);
141+
GC.SuppressFinalize(this);
93142
}
94143

95144
protected void HandleFilter(ChangeEventArgs args)
@@ -104,4 +153,9 @@ protected void HandleClear(string? value)
104153
{
105154
filter = value ?? string.Empty;
106155
}
156+
157+
protected void ViewErrorStructuredLogs(TResource resource)
158+
{
159+
NavigationManager.NavigateTo($"/StructuredLogs/{resource.Uid}?level=error");
160+
}
107161
}

src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
3131
<span slot="end">Level:</span>
3232
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
33-
<FluentSelect TOption="SelectViewModel<LogLevel>"
33+
<FluentSelect TOption="SelectViewModel<LogLevel?>"
3434
Items="@_logLevels"
3535
OptionText="@(c => c.Name)"
3636
@bind-SelectedOption="_selectedLogLevel"

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

+50-13
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ public partial class StructuredLogs
1919

2020
private TotalItemsFooter _totalItemsFooter = default!;
2121
private List<SelectViewModel<string>> _applications = default!;
22-
private List<SelectViewModel<LogLevel>> _logLevels = default!;
22+
private List<SelectViewModel<LogLevel?>> _logLevels = default!;
2323
private SelectViewModel<string> _selectedApplication = s_allApplication;
24-
private SelectViewModel<LogLevel> _selectedLogLevel = default!;
24+
private SelectViewModel<LogLevel?> _selectedLogLevel = default!;
2525
private Subscription? _applicationsSubscription;
2626
private Subscription? _logsSubscription;
2727
private bool _applicationChanged;
@@ -48,6 +48,10 @@ public partial class StructuredLogs
4848
[SupplyParameterFromQuery]
4949
public string? SpanId { get; set; }
5050

51+
[Parameter]
52+
[SupplyParameterFromQuery(Name = "level")]
53+
public string? LogLevelText { get; set; }
54+
5155
private ValueTask<GridItemsProviderResult<OtlpLogEntry>> GetData(GridItemsProviderRequest<OtlpLogEntry> request)
5256
{
5357
ViewModel.StartIndex = request.StartIndex;
@@ -59,6 +63,8 @@ private ValueTask<GridItemsProviderResult<OtlpLogEntry>> GetData(GridItemsProvid
5963
// The workaround is to put the count inside a control and explicitly update and refresh the control.
6064
_totalItemsFooter.SetTotalItemCount(logs.TotalItemCount);
6165

66+
TelemetryRepository.MarkViewedErrorLogs(ViewModel.ApplicationServiceId);
67+
6268
return ValueTask.FromResult(GridItemsProviderResult.From(logs.Items, logs.TotalItemCount));
6369
}
6470

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

76-
_logLevels = new List<SelectViewModel<LogLevel>>
82+
_logLevels = new List<SelectViewModel<LogLevel?>>
7783
{
78-
new SelectViewModel<LogLevel> { Id = LogLevel.Trace, Name = "(All)" },
79-
new SelectViewModel<LogLevel> { Id = LogLevel.Trace, Name = "Trace" },
80-
new SelectViewModel<LogLevel> { Id = LogLevel.Debug, Name = "Debug" },
81-
new SelectViewModel<LogLevel> { Id = LogLevel.Information, Name = "Information" },
82-
new SelectViewModel<LogLevel> { Id = LogLevel.Warning, Name = "Warning" },
83-
new SelectViewModel<LogLevel> { Id = LogLevel.Error, Name = "Error" },
84-
new SelectViewModel<LogLevel> { Id = LogLevel.Critical, Name = "Critical" },
84+
new SelectViewModel<LogLevel?> { Id = null, Name = "(All)" },
85+
new SelectViewModel<LogLevel?> { Id = LogLevel.Trace, Name = "Trace" },
86+
new SelectViewModel<LogLevel?> { Id = LogLevel.Debug, Name = "Debug" },
87+
new SelectViewModel<LogLevel?> { Id = LogLevel.Information, Name = "Information" },
88+
new SelectViewModel<LogLevel?> { Id = LogLevel.Warning, Name = "Warning" },
89+
new SelectViewModel<LogLevel?> { Id = LogLevel.Error, Name = "Error" },
90+
new SelectViewModel<LogLevel?> { Id = LogLevel.Critical, Name = "Critical" },
8591
};
8692
_selectedLogLevel = _logLevels[0];
8793

@@ -99,6 +105,17 @@ protected override void OnParametersSet()
99105
{
100106
_selectedApplication = _applications.SingleOrDefault(e => e.Id == ApplicationInstanceId) ?? s_allApplication;
101107
ViewModel.ApplicationServiceId = _selectedApplication.Id;
108+
109+
if (LogLevelText != null && Enum.TryParse<LogLevel>(LogLevelText, ignoreCase: true, out var logLevel))
110+
{
111+
_selectedLogLevel = _logLevels.SingleOrDefault(e => e.Id == logLevel) ?? _logLevels[0];
112+
}
113+
else
114+
{
115+
_selectedLogLevel = _logLevels[0];
116+
}
117+
ViewModel.LogLevel = _selectedLogLevel.Id;
118+
102119
UpdateSubscription();
103120
}
104121

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

111128
private Task HandleSelectedApplicationChangedAsync()
112129
{
113-
NavigationManager.NavigateTo($"/StructuredLogs/{_selectedApplication.Id}");
130+
NavigateTo(_selectedApplication.Id, _selectedLogLevel.Id);
114131
_applicationChanged = true;
115132

116133
return Task.CompletedTask;
117134
}
118135

119136
private Task HandleSelectedLogLevelChangedAsync()
120137
{
121-
ViewModel.LogLevel = _selectedLogLevel.Id;
138+
NavigateTo(_selectedApplication.Id, _selectedLogLevel.Id);
122139
_applicationChanged = true;
123140

124141
return Task.CompletedTask;
@@ -130,7 +147,7 @@ private void UpdateSubscription()
130147
if (_logsSubscription is null || _logsSubscription.ApplicationId != _selectedApplication.Id)
131148
{
132149
_logsSubscription?.Dispose();
133-
_logsSubscription = TelemetryRepository.OnNewLogs(_selectedApplication.Id, async () =>
150+
_logsSubscription = TelemetryRepository.OnNewLogs(_selectedApplication.Id, SubscriptionType.Read, async () =>
134151
{
135152
ViewModel.ClearData();
136153
await InvokeAsync(StateHasChanged);
@@ -221,6 +238,26 @@ private void HandleClear(string value)
221238
StateHasChanged();
222239
}
223240

241+
private void NavigateTo(string? applicationId, LogLevel? level)
242+
{
243+
string url;
244+
if (applicationId != null)
245+
{
246+
url = $"/StructuredLogs/{applicationId}";
247+
}
248+
else
249+
{
250+
url = $"/StructuredLogs";
251+
}
252+
253+
if (level != null)
254+
{
255+
url += $"?level={level.Value.ToString().ToLowerInvariant()}";
256+
}
257+
258+
NavigationManager.NavigateTo(url);
259+
}
260+
224261
private static string GetRowClass(OtlpLogEntry entry)
225262
{
226263
return $"log-row-{entry.Severity.ToString().ToLowerInvariant()}";

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private void UpdateDetailViewData()
121121
if (_tracesSubscription is null || _tracesSubscription.ApplicationId != _trace.FirstSpan.Source.InstanceId)
122122
{
123123
_tracesSubscription?.Dispose();
124-
_tracesSubscription = TelemetryRepository.OnNewTraces(_trace.FirstSpan.Source.InstanceId, () => InvokeAsync(() =>
124+
_tracesSubscription = TelemetryRepository.OnNewTraces(_trace.FirstSpan.Source.InstanceId, SubscriptionType.Read, () => InvokeAsync(() =>
125125
{
126126
UpdateDetailViewData();
127127
StateHasChanged();

0 commit comments

Comments
 (0)