Skip to content

Commit

Permalink
Add support for trace filters UI in the dashboard (#5751)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Sep 21, 2024
1 parent dc1eeaa commit 1bb4fb5
Show file tree
Hide file tree
Showing 54 changed files with 1,554 additions and 255 deletions.
8 changes: 4 additions & 4 deletions src/Aspire.Dashboard/Aspire.Dashboard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@
<AutoGen>True</AutoGen>
<DependentUpon>Routes.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Logs.Designer.cs">
<Compile Update="Resources\StructuredFiltering.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Logs.resx</DependentUpon>
<DependentUpon>StructuredFiltering.resx</DependentUpon>
</Compile>
</ItemGroup>

Expand Down Expand Up @@ -212,12 +212,12 @@
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Routes.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Logs.resx">
<EmbeddedResource Update="Resources\StructuredFiltering.resx">
<XlfSourceFormat>Resx</XlfSourceFormat>
<XlfOutputItem>EmbeddedResource</XlfOutputItem>
<SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Logs.Designer.cs</LastGenOutput>
<LastGenOutput>StructuredFiltering.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Components/Dialogs/FilterDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
@implements IDialogContentComponent<FilterDialogViewModel>

@inject IStringLocalizer<Dialogs> Loc
@inject IStringLocalizer<Logs> LogsLoc
@inject IStringLocalizer<StructuredFiltering> FilterLoc

<EditForm EditContext="@EditContext" OnValidSubmit="@Apply">
<DataAnnotationsValidator />
Expand Down Expand Up @@ -33,7 +33,7 @@

<div class="filter-input-container">
<FluentTextField @bind-Value="_formModel.Value"
Label="@Loc[nameof(Dialogs.FilterDialogTextValuePlaceholder)]" />
Label="@Loc[nameof(Dialogs.FilterDialogTextValuePlaceholder)]" Style="width:100%" />
<ValidationMessage For="() => _formModel.Value" />
</div>

Expand Down
30 changes: 18 additions & 12 deletions src/Aspire.Dashboard/Components/Dialogs/FilterDialog.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public partial class FilterDialog
private List<SelectViewModel<FilterCondition>> _filterConditions = null!;

private SelectViewModel<FilterCondition> CreateFilterSelectViewModel(FilterCondition condition) =>
new SelectViewModel<FilterCondition> { Id = condition, Name = LogFilter.ConditionToString(condition, LogsLoc) };
new SelectViewModel<FilterCondition> { Id = condition, Name = TelemetryFilter.ConditionToString(condition, FilterLoc) };

[CascadingParameter]
public FluentDialog? Dialog { get; set; }
Expand Down Expand Up @@ -47,16 +47,22 @@ protected override void OnInitialized()

protected override void OnParametersSet()
{
var names = new List<string> { LogFilter.KnownMessageField, LogFilter.KnownCategoryField, LogFilter.KnownApplicationField, LogFilter.KnownTraceIdField, LogFilter.KnownSpanIdField, LogFilter.KnownOriginalFormatField };
var knownFields = names.Select(p => new SelectViewModel<string> { Id = p, Name = LogFilter.ResolveFieldName(p) }).ToList();
var customFields = Content.LogPropertyKeys.Select(p => new SelectViewModel<string> { Id = p, Name = LogFilter.ResolveFieldName(p) }).ToList();
var knownFields = Content.KnownKeys.Select(p => new SelectViewModel<string> { Id = p, Name = TelemetryFilter.ResolveFieldName(p) }).ToList();
var customFields = Content.PropertyKeys.Select(p => new SelectViewModel<string> { Id = p, Name = TelemetryFilter.ResolveFieldName(p) }).ToList();

_parameters =
[
.. knownFields,
new SelectViewModel<string> { Id = null, Name = "-" },
.. customFields
];
if (customFields.Count > 0)
{
_parameters =
[
.. knownFields,
new SelectViewModel<string> { Id = null, Name = "-" },
.. customFields
];
}
else
{
_parameters = knownFields;
}

if (Content.Filter is { } logFilter)
{
Expand All @@ -66,7 +72,7 @@ .. customFields
}
else
{
_formModel.Parameter = _parameters.SingleOrDefault(c => c.Id == LogFilter.KnownMessageField);
_formModel.Parameter = _parameters.FirstOrDefault();
_formModel.Condition = _filterConditions.Single(c => c.Id == FilterCondition.Contains);
_formModel.Value = "";
}
Expand Down Expand Up @@ -94,7 +100,7 @@ private void Apply()
}
else
{
var filter = new LogFilter
var filter = new TelemetryFilter
{
Field = _formModel.Parameter!.Id!,
Condition = _formModel.Condition!.Id,
Expand Down
14 changes: 7 additions & 7 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

@inject IStringLocalizer<Dashboard.Resources.StructuredLogs> Loc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc
@inject IStringLocalizer<Logs> LogsLoc
@inject IStringLocalizer<StructuredFiltering> FilterLoc

<PageTitle><ApplicationName ResourceName="@nameof(Dashboard.Resources.StructuredLogs.StructuredLogsPageTitle)" Loc="@Loc" /></PageTitle>

Expand Down Expand Up @@ -56,28 +56,28 @@
HandleSelectedLogLevelChangedAsync="@HandleSelectedLogLevelChangedAsync" />
}
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<FluentLabel slot="end">@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsFilters)]</FluentLabel>
<FluentLabel slot="end">@FilterLoc[nameof(StructuredFiltering.Filters)]</FluentLabel>
@if (ViewModel.Filters.Count == 0)
{
<span slot="end">@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsNoFilters)]</span>
<span slot="end">@FilterLoc[nameof(StructuredFiltering.NoFilters)]</span>
}
else
{
foreach (var filter in ViewModel.Filters)
{
<FluentButton slot="end" Appearance="Appearance.Outline" OnClick="() => OpenFilterAsync(filter)">@filter.GetDisplayText(LogsLoc)</FluentButton>
<FluentButton slot="end" Appearance="Appearance.Outline" OnClick="() => OpenFilterAsync(filter)">@filter.GetDisplayText(FilterLoc)</FluentButton>
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
}
}

@if (ViewportInformation.IsDesktop)
{
<FluentButton slot="end" Appearance="Appearance.Stealth" aria-label="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsAddFilter)]" OnClick="() => OpenFilterAsync(null)"><FluentIcon Value="@(new Icons.Regular.Size16.Filter())" /></FluentButton>
<FluentButton slot="end" Appearance="Appearance.Stealth" aria-label="@FilterLoc[nameof(StructuredFiltering.AddFilter)]" OnClick="() => OpenFilterAsync(null)"><FluentIcon Value="@(new Icons.Regular.Size16.Filter())" /></FluentButton>
}
else
{
<FluentButton slot="end" Appearance="Appearance.Stealth" aria-label="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsAddFilter)]" OnClick="() => OpenFilterAsync(null)">
<FluentIcon Class="align-text-bottom" Value="@(new Icons.Regular.Size16.Filter())" /> @Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsAddFilter)]
<FluentButton slot="end" Appearance="Appearance.Stealth" aria-label="@FilterLoc[nameof(StructuredFiltering.AddFilter)]" OnClick="() => OpenFilterAsync(null)">
<FluentIcon Class="align-text-bottom" Value="@(new Icons.Regular.Size16.Filter())" /> @Loc[nameof(StructuredFiltering.AddFilter)]
</FluentButton>
}
</ToolbarSection>
Expand Down
24 changes: 13 additions & 11 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -143,16 +144,16 @@ protected override Task OnInitializedAsync()

if (!string.IsNullOrEmpty(TraceId))
{
ViewModel.AddFilter(new LogFilter
ViewModel.AddFilter(new TelemetryFilter
{
Field = LogFilter.KnownTraceIdField, Condition = FilterCondition.Equals, Value = TraceId
Field = KnownStructuredLogFields.TraceIdField, Condition = FilterCondition.Equals, Value = TraceId
});
}
if (!string.IsNullOrEmpty(SpanId))
{
ViewModel.AddFilter(new LogFilter
ViewModel.AddFilter(new TelemetryFilter
{
Field = LogFilter.KnownSpanIdField, Condition = FilterCondition.Equals, Value = SpanId
Field = KnownStructuredLogFields.SpanIdField, Condition = FilterCondition.Equals, Value = SpanId
});
}

Expand Down Expand Up @@ -265,34 +266,35 @@ private async Task ClearSelectedLogEntryAsync(bool causedByUserAction = false)
_elementIdBeforeDetailsViewOpened = null;
}

private async Task OpenFilterAsync(LogFilter? entry)
private async Task OpenFilterAsync(TelemetryFilter? entry)
{
if (_contentLayout is not null)
{
await _contentLayout.CloseMobileToolbarAsync();
}

var logPropertyKeys = TelemetryRepository.GetLogPropertyKeys(PageViewModel.SelectedApplication.Id?.GetApplicationKey());

var title = entry is not null ? Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEditFilter)] : Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsAddFilter)];
var title = entry is not null ? FilterLoc[nameof(StructuredFiltering.DialogTitleEditFilter)] : FilterLoc[nameof(StructuredFiltering.DialogTitleAddFilter)];
var parameters = new DialogParameters
{
OnDialogResult = DialogService.CreateDialogCallback(this, HandleFilterDialog),
Title = title,
Alignment = HorizontalAlignment.Right,
PrimaryAction = null,
SecondaryAction = null,
Width = "450px"
};
var data = new FilterDialogViewModel
{
Filter = entry, LogPropertyKeys = logPropertyKeys
Filter = entry,
PropertyKeys = TelemetryRepository.GetLogPropertyKeys(PageViewModel.SelectedApplication.Id?.GetApplicationKey()),
KnownKeys = KnownStructuredLogFields.AllFields
};
await DialogService.ShowPanelAsync<FilterDialog>(data, parameters);
}

private async Task HandleFilterDialog(DialogResult result)
{
if (result.Data is FilterDialogResult filterResult && filterResult.Filter is LogFilter filter)
if (result.Data is FilterDialogResult filterResult && filterResult.Filter is TelemetryFilter filter)
{
if (filterResult.Delete)
{
Expand Down Expand Up @@ -460,6 +462,6 @@ public class StructuredLogsPageState
{
public string? SelectedApplication { get; set; }
public string? LogLevelText { get; set; }
public required IReadOnlyCollection<LogFilter> Filters { get; set; }
public required IReadOnlyCollection<TelemetryFilter> Filters { get; set; }
}
}
26 changes: 26 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Traces.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@using Aspire.Dashboard.Components.Controls.Grid
@inject IJSRuntime JS
@inject IStringLocalizer<Dashboard.Resources.Traces> Loc
@inject IStringLocalizer<StructuredFiltering> FilterLoc
@inject IStringLocalizer<ControlsStrings> ControlsStringsLoc
@implements IDisposable

Expand All @@ -30,6 +31,31 @@
Placeholder="@ControlsStringsLoc[nameof(ControlsStrings.FilterPlaceholder)]"
title="@Loc[nameof(Dashboard.Resources.Traces.TracesNameFilter)]"
slot="end" />
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<FluentLabel slot="end">@FilterLoc[nameof(StructuredFiltering.Filters)]</FluentLabel>
@if (TracesViewModel.Filters.Count == 0)
{
<span slot="end">@FilterLoc[nameof(StructuredFiltering.NoFilters)]</span>
}
else
{
foreach (var filter in TracesViewModel.Filters)
{
<FluentButton slot="end" Appearance="Appearance.Outline" OnClick="() => OpenFilterAsync(filter)">@filter.GetDisplayText(FilterLoc)</FluentButton>
<FluentDivider slot="end" Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
}
}

@if (ViewportInformation.IsDesktop)
{
<FluentButton slot="end" Appearance="Appearance.Stealth" aria-label="@FilterLoc[nameof(StructuredFiltering.AddFilter)]" OnClick="() => OpenFilterAsync(null)"><FluentIcon Value="@(new Icons.Regular.Size16.Filter())" /></FluentButton>
}
else
{
<FluentButton slot="end" Appearance="Appearance.Stealth" aria-label="@FilterLoc[nameof(StructuredFiltering.AddFilter)]" OnClick="() => OpenFilterAsync(null)">
<FluentIcon Class="align-text-bottom" Value="@(new Icons.Regular.Size16.Filter())" /> @Loc[nameof(StructuredFiltering.AddFilter)]
</FluentButton>
}
</ToolbarSection>
<MainSection>
<div class="datagrid-overflow-area continuous-scroll-overflow" tabindex="-1">
Expand Down
79 changes: 76 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/Traces.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Components.Layout;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Extensions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
Expand All @@ -14,11 +16,10 @@
using Microsoft.Extensions.Options;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
using static Aspire.Dashboard.Components.Pages.Traces;

namespace Aspire.Dashboard.Components.Pages;

public partial class Traces : IPageWithSessionAndUrlState<TracesPageViewModel, TracesPageState>
public partial class Traces : IPageWithSessionAndUrlState<Traces.TracesPageViewModel, Traces.TracesPageState>
{
private const string TimestampColumn = nameof(TimestampColumn);
private const string NameColumn = nameof(NameColumn);
Expand Down Expand Up @@ -79,6 +80,10 @@ public partial class Traces : IPageWithSessionAndUrlState<TracesPageViewModel, T
[CascadingParameter]
public required ViewportInformation ViewportInformation { get; set; }

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

private string GetNameTooltip(OtlpTrace trace)
{
var tooltip = string.Format(CultureInfo.InvariantCulture, Loc[nameof(Dashboard.Resources.Traces.TracesFullName)], trace.FullName);
Expand Down Expand Up @@ -267,21 +272,88 @@ public void UpdateViewModelFromQuery(TracesPageViewModel viewModel)
{
viewModel.SelectedApplication = _applicationViewModels.GetApplication(Logger, ApplicationName, canSelectGrouping: true, _allApplication);
TracesViewModel.ApplicationKey = PageViewModel.SelectedApplication.Id?.GetApplicationKey();

if (SerializedLogFilters is not null)
{
var filters = LogFilterFormatter.DeserializeLogFiltersFromString(SerializedLogFilters);

if (filters.Count > 0)
{
TracesViewModel.ClearFilters();
foreach (var filter in filters)
{
TracesViewModel.AddFilter(filter);
}
}
}

_ = Task.Run(async () =>
{
await InvokeAsync(StateHasChanged);
});
}

public string GetUrlFromSerializableViewModel(TracesPageState serializable)
{
return DashboardUrls.TracesUrl(serializable.SelectedApplication);
var filters = (serializable.Filters.Count > 0) ? LogFilterFormatter.SerializeLogFiltersToString(serializable.Filters) : null;

return DashboardUrls.TracesUrl(
resource: serializable.SelectedApplication,
filters: filters);
}

public TracesPageState ConvertViewModelToSerializable()
{
return new TracesPageState
{
SelectedApplication = PageViewModel.SelectedApplication.Id is not null ? PageViewModel.SelectedApplication.Name : null,
Filters = TracesViewModel.Filters
};
}

private async Task OpenFilterAsync(TelemetryFilter? entry)
{
if (_contentLayout is not null)
{
await _contentLayout.CloseMobileToolbarAsync();
}

var title = entry is not null ? FilterLoc[nameof(StructuredFiltering.DialogTitleEditFilter)] : FilterLoc[nameof(StructuredFiltering.DialogTitleAddFilter)];
var parameters = new DialogParameters
{
OnDialogResult = DialogService.CreateDialogCallback(this, HandleFilterDialog),
Title = title,
Alignment = HorizontalAlignment.Right,
PrimaryAction = null,
SecondaryAction = null,
Width = "450px"
};
var data = new FilterDialogViewModel
{
Filter = entry,
PropertyKeys = TelemetryRepository.GetTracePropertyKeys(PageViewModel.SelectedApplication.Id?.GetApplicationKey()),
KnownKeys = KnownTraceFields.AllFields,
};
await DialogService.ShowPanelAsync<FilterDialog>(data, parameters);
}

private async Task HandleFilterDialog(DialogResult result)
{
if (result.Data is FilterDialogResult filterResult && filterResult.Filter is TelemetryFilter filter)
{
if (filterResult.Delete)
{
TracesViewModel.RemoveFilter(filter);
}
else if (filterResult.Add)
{
TracesViewModel.AddFilter(filter);
}
}

await this.AfterViewModelChangedAsync(_contentLayout, isChangeInToolbar: true);
}

public class TracesPageViewModel
{
public required SelectViewModel<ResourceTypeDetails> SelectedApplication { get; set; }
Expand All @@ -290,5 +362,6 @@ public class TracesPageViewModel
public class TracesPageState
{
public string? SelectedApplication { get; set; }
public required IReadOnlyCollection<TelemetryFilter> Filters { get; set; }
}
}
Loading

0 comments on commit 1bb4fb5

Please sign in to comment.