Skip to content

Commit

Permalink
Use gRPC to communicate data from app host to dashboard (#1476)
Browse files Browse the repository at this point in the history
* Model per-type resource data as extension data

The UI will have a single object type for all resources, treating the uniformly. That object has both a fixed set of fields, and now its own extensible metadata.

In the protocol between ResourceService and Dashboard, we'll need an extensible way to model these resource data. This allows the UI to work with resource types it hasn't seen before.

* Split snapshots from view models

We are moving towards removing `Aspire.Hosting`'s reference to `Aspire.Dashboard`. In this new model we will have different types on the back end and front end.

- DCP has its own types (e.g. `CustomResource`), which remain untouched, and exist only on the back end.
- `ResourceSnapshot` is a new abstract base class for `ContainerSnapshot`, `ExecutableSnapshot` and `ProjectSnapshot`. These are immutable, have no view-specific data, and will only be used on the back end. In the near future, these objects will be used to populate gRPC messages.
- `ResourceViewModel` remains, though only on the front end. It no longer has subclasses. We are designing the dashboard to support arbitrary resource types. In the near future, these objects will be constructed and updated in response to gRPC messages.

As the front end no longer has subclasses of `ResourceViewModel`, some of the hard-coded resource-specific logic has been changed to work on the additional data within a resource.

We also introduce a `KnownResourceTypes` static class with consts for known resource type names (e.g. "Container").

* Remove obsolete TODOs
* Rename ResourceDataKeys to KnownProperties
* Move types to their own source files
* Split endpoint snapshot from view model
* Consistent naming in razor files
* Add ctor to EnvironmentVariableViewModel
* Complete ResourceViewModel construction from Resource message
* Make types internal
* Add words to dictionary
* Use KnownResourceTypes
* Pass correct type to logger factory
* Rename ResourceService to DashboardClient

* Dashboard receives data via gRPC

Uses the gRPC `.proto` file added in a previous commit (with some small changes) to model data sent between the back end (app host) and front end (dashboard). Currently this occurs within the same process, but future work will split the dashboard out of the app host.

- Object flow is now "DCP -> snapshots -> gRPC messages -> view models".
- Move `DashboardWebApplication` initialization out of `DcpHostService` and into the new `DashboardWebApplicationHost` in preparation for future split.
- Split `ResourceSubscription`/`ResourceChange`/`ResourceChangeType` into both snapshot and view model versions, for use on back/front ends respectively.

* Add API doc
* Logging
* Don't treat cancellations as errors
* Join async connection management on dispose
* Throw on unexpected values
* Log when dashboard is disabled
* Remove redundant route on gRPC endpoint
* Log reason when dashboard hosting is skipped
* Handle null container ID
* Extract common check

* Specify assembly name for eShopLite's app host

The app host exposes this via `Microsoft.Extensions.Hosting.IHostEnvironment.ApplicationName`, which is documented as returning the assembly name. Any `.AppHost` suffix is removed before the name returned.

Because the name of this project is just `AppHost`, the dashboard for this project would only show `AppHost`, which is misleading.

This sets the assembly name to have the expected naming convention, without requiring the project name to change and look different to sibling projects in eShopLite.

* Ensure the page title displays

With the introduction of gRPC between dashboard and app host, the dashboard can render its UI before the application information request returns.

One negative consequence of this is that the UI's page title and header area do not show the application name. When this data arrives later asynchronously, the UI is not updated.

This change introduces a way for the UI to refresh at a later time, once the application name is available.

* Remove application version from protocol

This was unused, and it's unclear how we'd populate it. We can add it back later if we want it again, but let's not bake it into the protocol just yet.

* Improve parsing of application name

This covers the case that the name does not have a period. For example, one of the sample apps in this repo is named `DaprAppHost`, and the name `Dapr` would be perfect, but the previous code required a period in the suffix (e.g. `.AppHost`).

* Fix potential ANE due to unannotated gRPC code
* Configure logging for dashboard web application
* Use concurrent dictionaries
* More spelling words
  • Loading branch information
drewnoakes authored Jan 5, 2024
1 parent 9f84cd7 commit 40ebf07
Show file tree
Hide file tree
Showing 54 changed files with 1,702 additions and 523 deletions.
1 change: 1 addition & 0 deletions samples/eShopLite/AppHost/AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<UserSecretsId>1bf0740a-0dfc-45aa-9002-def9b2b17da0</UserSecretsId>
<ImplicitUsings>enable</ImplicitUsings>
<IsAspireHost>true</IsAspireHost>
<AssemblyName>eShopLite.AppHost</AssemblyName>
</PropertyGroup>

<ItemGroup>
Expand Down
9 changes: 9 additions & 0 deletions spelling.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
ansi
apigateway
apiserver
blazor
brightblack
brightblue
brightcyan
Expand All @@ -8,16 +11,22 @@ brightred
brightwhite
brightyellow
dbug
dcpctrl
dylib
equatable
grpc
kubeconfig
Kubelet
kubernetes
libc
noreferrer
noopener
oninput
otlp
protoc
redis
runtimeconfig
trce
upsert
uris
urls
1 change: 1 addition & 0 deletions src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
<Protobuf Include="..\Aspire.Hosting\Dashboard\proto\resource_service.proto" GrpcServices="Client" Access="Internal">
<Link>Protos\resource_service.proto</Link>
</Protobuf>
<Compile Include="..\Aspire.Hosting\Extensions\ChannelExtensions.cs" Link="Extensions\ChannelExtensions.cs" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<FluentHeader>
<div class="header-title">
<FluentAnchor IconStart="@(new AspireIcons.Size32.Logo())" Appearance="Appearance.Stealth" Href="/" Class="logo">
@string.Format(Loc[nameof(Layout.MainLayoutDashboardName)], ResourceService.ApplicationName)
@DashboardClient.FormatApplicationName(Loc[nameof(Layout.MainLayoutDashboardName)], () => InvokeAsync(StateHasChanged))
</FluentAnchor>
</div>
<div class="header-right">
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public partial class MainLayout : IDisposable
public required IStringLocalizer<Resources.Layout> Loc { get; set; }

[Inject]
public required IResourceService ResourceService { get; set; }
public required IDashboardClient DashboardClient { get; set; }

[Inject]
public required IDialogService DialogService { get; set; }
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
@page "/ConsoleLogs/{resourceName?}"
@namespace Aspire.Dashboard.Components.Pages
@using Aspire.Dashboard.Model
@inject IStringLocalizer<Dashboard.Resources.ConsoleLogs> Loc

<PageTitle>@string.Format(Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsPageTitle)], ResourceService.ApplicationName)</PageTitle>
<PageTitle>@DashboardClient.FormatApplicationName(Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsPageTitle)], () => InvokeAsync(StateHasChanged))</PageTitle>

<div class="resource-logs-layout">
<h1 class="page-header">@Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsHeader)]</h1>
Expand Down
16 changes: 8 additions & 8 deletions src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Aspire.Dashboard.Components.Pages;
public sealed partial class ConsoleLogs : ComponentBase, IAsyncDisposable
{
[Inject]
public required IResourceService ResourceService { get; init; }
public required IDashboardClient DashboardClient { get; init; }
[Inject]
public required IJSRuntime JS { get; init; }
[Inject]
Expand Down Expand Up @@ -50,7 +50,7 @@ protected override void OnInitialized()

void TrackResources()
{
var (snapshot, subscription) = ResourceService.SubscribeResources();
var (snapshot, subscription) = DashboardClient.SubscribeResources();

foreach (var resource in snapshot)
{
Expand Down Expand Up @@ -150,13 +150,13 @@ private async ValueTask LoadLogsAsync()
{
var cancellationToken = await _logSubscriptionCancellationSeries.NextAsync();

var subscription = ResourceService.SubscribeConsoleLogs(_selectedResource.Name, cancellationToken);
var subscription = DashboardClient.SubscribeConsoleLogs(_selectedResource.Name, cancellationToken);

if (subscription is not null)
{
var task = _logViewer.SetLogSourceAsync(
subscription,
convertTimestampsFromUtc: _selectedResource is ContainerViewModel);
convertTimestampsFromUtc: _selectedResource.IsContainer());

_initialisedSuccessfully = true;
_status = Loc[nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsWatchingLogs)];
Expand All @@ -171,7 +171,7 @@ private async ValueTask LoadLogsAsync()
else
{
_initialisedSuccessfully = false;
_status = Loc[_selectedResource is ContainerViewModel
_status = Loc[_selectedResource.IsContainer()
? nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsFailedToInitialize)
: nameof(Dashboard.Resources.ConsoleLogs.ConsoleLogsLogsNotYetAvailable)];
}
Expand All @@ -185,9 +185,9 @@ private async Task HandleSelectedOptionChangedAsync()
NavigationManager.NavigateTo($"/ConsoleLogs/{_selectedOption?.Value}");
}

private async Task OnResourceChanged(ResourceChangeType changeType, ResourceViewModel resource)
private async Task OnResourceChanged(ResourceViewModelChangeType changeType, ResourceViewModel resource)
{
if (changeType == ResourceChangeType.Upsert)
if (changeType == ResourceViewModelChangeType.Upsert)
{
_resourceByName[resource.Name] = resource;

Expand All @@ -206,7 +206,7 @@ private async Task OnResourceChanged(ResourceChangeType changeType, ResourceView
}
}
}
else if (changeType == ResourceChangeType.Delete)
else if (changeType == ResourceViewModelChangeType.Delete)
{
var removed = _resourceByName.TryRemove(resource.Name, out _);
Debug.Assert(removed, "Cannot remove unknown resource.");
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Components/Pages/Metrics.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
@page "/Metrics/{applicationInstanceId}/Meter/{meterName}"
@page "/Metrics/{applicationInstanceId}/Meter/{meterName}/Instrument/{instrumentName}"

@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Model.Otlp
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Otlp.Model
@inject IStringLocalizer<Dashboard.Resources.Metrics> Loc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc

<PageTitle>@string.Format(Loc[nameof(Dashboard.Resources.Metrics.MetricsPageTitle)], ResourceService.ApplicationName)</PageTitle>
<PageTitle>@DashboardClient.FormatApplicationName(Loc[nameof(Dashboard.Resources.Metrics.MetricsPageTitle)], () => InvokeAsync(StateHasChanged))</PageTitle>

<div class="metrics-layout">
<h1 class="page-header">@Loc[nameof(Dashboard.Resources.Metrics.MetricsHeader)]</h1>
Expand Down
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 @@ -44,7 +44,7 @@ public partial class Metrics : IDisposable
public required NavigationManager NavigationManager { get; set; }

[Inject]
public required IResourceService ResourceService { get; set; }
public required IDashboardClient DashboardClient { get; set; }

[Inject]
public required ProtectedSessionStorage ProtectedSessionStore { get; set; }
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
@page "/"
@using Aspire.Dashboard.Components.ResourcesGridColumns
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<Dashboard.Resources.Resources> Loc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc

<PageTitle>@string.Format(Loc[nameof(Dashboard.Resources.Resources.ResourcesPageTitle)], ResourceService.ApplicationName)</PageTitle>
<PageTitle>@DashboardClient.FormatApplicationName(Loc[nameof(Dashboard.Resources.Resources.ResourcesPageTitle)], () => InvokeAsync(StateHasChanged))</PageTitle>

<div class="content-layout-with-toolbar">
<FluentToolbar Orientation="Orientation.Horizontal">
Expand Down
35 changes: 14 additions & 21 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public partial class Resources : ComponentBase, IDisposable
private Dictionary<OtlpApplication, int>? _applicationUnviewedErrorCounts;

[Inject]
public required IResourceService ResourceService { get; init; }
public required IDashboardClient DashboardClient { get; init; }
[Inject]
public required TelemetryRepository TelemetryRepository { get; init; }
[Inject]
Expand All @@ -31,7 +31,7 @@ public partial class Resources : ComponentBase, IDisposable
private readonly CancellationTokenSource _watchTaskCancellationTokenSource = new();
private readonly ConcurrentDictionary<string, ResourceViewModel> _resourceByName = new(StringComparers.ResourceName);
// TODO populate resource types from server data
private readonly ImmutableArray<string> _allResourceTypes = ["Project", "Executable", "Container"];
private readonly ImmutableArray<string> _allResourceTypes = [KnownResourceTypes.Project, KnownResourceTypes.Executable, KnownResourceTypes.Container];
private readonly HashSet<string> _visibleResourceTypes;
private string _filter = "";
private bool _isTypeFilterVisible;
Expand Down Expand Up @@ -87,7 +87,7 @@ protected override void OnInitialized()
{
_applicationUnviewedErrorCounts = TelemetryRepository.GetApplicationUnviewedErrorLogsCount();

var (snapshot, subscription) = ResourceService.SubscribeResources();
var (snapshot, subscription) = DashboardClient.SubscribeResources();

foreach (var resource in snapshot)
{
Expand All @@ -99,7 +99,17 @@ protected override void OnInitialized()
{
await foreach (var (changeType, resource) in subscription.WithCancellation(_watchTaskCancellationTokenSource.Token))
{
await OnResourceListChanged(changeType, resource);
if (changeType == ResourceViewModelChangeType.Upsert)
{
_resourceByName[resource.Name] = resource;
}
else if (changeType == ResourceViewModelChangeType.Delete)
{
var removed = _resourceByName.TryRemove(resource.Name, out _);
Debug.Assert(removed, "Cannot remove unknown resource.");
}
await InvokeAsync(StateHasChanged);
}
});

Expand Down Expand Up @@ -129,23 +139,6 @@ private void ClearSelectedResource()
SelectedResource = null;
}

private async Task OnResourceListChanged(ResourceChangeType changeType, ResourceViewModel resource)
{
switch (changeType)
{
case ResourceChangeType.Upsert:
_resourceByName[resource.Name] = resource;
break;

case ResourceChangeType.Delete:
var removed = _resourceByName.TryRemove(resource.Name, out _);
Debug.Assert(removed, "Cannot remove unknown resource.");
break;
}

await InvokeAsync(StateHasChanged);
}

private string GetResourceName(ResourceViewModel resource) => ResourceViewModel.GetResourceName(resource, _resourceByName.Values);

private bool HasMultipleReplicas(ResourceViewModel resource)
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
@using System.Web
@using Aspire.Dashboard.Resources
@inject NavigationManager NavigationManager
@inject IResourceService resourceService
@inject IDashboardClient DashboardClient
@inject IJSRuntime JS
@implements IDisposable
@inject IStringLocalizer<Dashboard.Resources.StructuredLogs> Loc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc

<PageTitle>@string.Format(Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsPageTitle)], resourceService.ApplicationName)</PageTitle>
<PageTitle>@DashboardClient.FormatApplicationName(Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsPageTitle)], () => InvokeAsync(StateHasChanged))</PageTitle>

<div class="logs-layout">
<h1 class="page-header">@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsHeader)]</h1>
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<Dashboard.Resources.TraceDetail> Loc
@inject IStringLocalizer<ControlsStrings> ControlStringsLoc
@inject IResourceService DashboardService
@inject IDashboardClient DashboardClient

<PageTitle>@string.Format(Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailPageTitle)], DashboardService.ApplicationName)</PageTitle>
<PageTitle>@DashboardClient.FormatApplicationName(Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailPageTitle)], () => InvokeAsync(StateHasChanged))</PageTitle>

@if (_trace != null)
{
Expand Down
5 changes: 2 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/Traces.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Resources
@inject NavigationManager NavigationManager
@inject IResourceService ResourceService
@inject IDashboardClient DashboardClient
@inject IJSRuntime JS
@inject IStringLocalizer<Dashboard.Resources.Traces> Loc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc
@inject IStringLocalizer<TraceDetail> TraceDetailLoc
@inject IStringLocalizer<Dashboard.Resources.StructuredLogs> StructuredLogsLoc
@implements IDisposable

<PageTitle>@string.Format(Loc[nameof(Dashboard.Resources.Traces.TracesPageTitle)], ResourceService.ApplicationName)</PageTitle>

<PageTitle>@DashboardClient.FormatApplicationName(Loc[nameof(Dashboard.Resources.Traces.TracesPageTitle)], () => InvokeAsync(StateHasChanged))</PageTitle>

<div class="traces-layout">
<h1 class="page-header">@Loc[nameof(Dashboard.Resources.Traces.TracesHeader)]</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@

<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<span><FluentHighlighter HighlightedText="@FilterText" Text="@FormatName(Resource)" /></span>
@if (Resource is ContainerViewModel containerViewModel)
@if (Resource.TryGetContainerId(out var containerId))
{
<div class="subtext">
<GridValue Value="@containerViewModel.ContainerId"
<GridValue Value="@containerId"
MaxDisplayLength="8"
EnableHighlighting="false"
PreCopyToolTip="@Loc[nameof(Columns.ResourceNameDisplayCopyContainerIdText)]"
ToolTip="@string.Format(Loc[nameof(Columns.ResourceNameDisplayContainerIdText)], containerViewModel.ContainerId)"/>
ToolTip="@string.Format(Loc[nameof(Columns.ResourceNameDisplayContainerIdText)], containerId)"/>
</div>
}
else if (Resource is ExecutableViewModel executableViewModel)
else if (Resource.TryGetProcessId(out int processId))
{
// NOTE projects are also executables, so this will handle both
var title = string.Format(Loc[nameof(Columns.ResourceNameDisplayProcessIdText)], executableViewModel.ProcessId);
<span class="subtext" title="@title" aria-label="@title">@executableViewModel.ProcessId</span>
var title = string.Format(Loc[nameof(Columns.ResourceNameDisplayProcessIdText)], processId);
<span class="subtext" title="@title" aria-label="@title">@processId</span>
}
</FluentStack>

Expand Down
Loading

0 comments on commit 40ebf07

Please sign in to comment.