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

Add span links to dashboard UI #4805

Merged
merged 2 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
81 changes: 78 additions & 3 deletions playground/Stress/Stress.ApiService/TraceCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ public class TraceCreator

private static readonly ActivitySource s_activitySource = new(ActivitySourceName);

private readonly List<Activity> _allActivities = new List<Activity>();

public async Task CreateTraceAsync(int count, bool createChildren)
{
for (var i = 0; i < count; i++)
var activityStack = new Stack<Activity>();

for (var i = 0; i < 10; i++)
{
if (i > 0)
{
Expand All @@ -27,19 +31,39 @@ public async Task CreateTraceAsync(int count, bool createChildren)
continue;
}

_allActivities.Add(activity);

if (createChildren)
{
await CreateChildActivityAsync(name);
}

await Task.Delay(Random.Shared.Next(10, 50));
}

while (activityStack.Count > 0)
{
activityStack.Pop().Stop();
}
}

private static async Task CreateChildActivityAsync(string parentName)
private async Task CreateChildActivityAsync(string parentName)
{
if (Random.Shared.NextDouble() > 0.05)
{
var name = parentName + "-0";
using var activity = s_activitySource.StartActivity(name, ActivityKind.Client);

var links = CreateLinks();

using var activity = s_activitySource.StartActivity(ActivityKind.Client, name: name, links: links.DistinctBy(l => l.Context.SpanId));
if (activity == null)
{
return;
}

AddEvents(activity);

_allActivities.Add(activity);

await Task.Delay(Random.Shared.Next(10, 50));

Expand All @@ -48,4 +72,55 @@ private static async Task CreateChildActivityAsync(string parentName)
await Task.Delay(Random.Shared.Next(10, 50));
}
}

private static void AddEvents(Activity activity)
{
var eventCount = Random.Shared.Next(0, 5);
for (var i = 0; i < eventCount; i++)
{
var activityTags = new ActivityTagsCollection();
var tagsCount = Random.Shared.Next(0, 3);
for (var j = 0; j < tagsCount; j++)
{
activityTags.Add($"key-{j}", "Value!");
}

activity.AddEvent(new ActivityEvent($"event-{i}", DateTimeOffset.UtcNow.AddMilliseconds(1), activityTags));
}
}

private ActivityLink[] CreateLinks()
{
var activityLinkCount = Random.Shared.Next(0, Math.Min(5, _allActivities.Count));
var links = new ActivityLink[activityLinkCount];
for (var i = 0; i < links.Length; i++)
{
// Randomly create some tags.
var activityTags = new ActivityTagsCollection();
var tagsCount = Random.Shared.Next(0, 3);
for (var j = 0; j < tagsCount; j++)
{
activityTags.Add($"key-{j}", "Value!");
}

// Create the activity link. There is a 50% chance the activity link goes to an activity
// that doesn't exist. This logic is here to ensure incomplete links are handled correctly.
ActivityContext activityContext;
if (Random.Shared.Next() % 2 == 0)
{
var a = _allActivities[Random.Shared.Next(0, _allActivities.Count)];
activityContext = a.Context;
}
else
{
activityContext = new ActivityContext(
ActivityTraceId.CreateRandom(),
ActivitySpanId.CreateRandom(),
ActivityTraceFlags.None);
}
links[i] = new ActivityLink(activityContext, activityTags);
}

return links;
}
}
7 changes: 1 addition & 6 deletions src/Aspire.Dashboard/Components/Controls/Chart/ChartBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ private void AddExemplars(List<ChartExemplar> exemplars, MetricValueBase metric)
var key = new SpanKey(exemplar.TraceId, exemplar.SpanId);
if (!_currentCache.TryGetValue(key, out var span))
{
span = GetSpan(exemplar.TraceId, exemplar.SpanId);
span = TelemetryRepository.GetSpan(exemplar.TraceId, exemplar.SpanId);
}
if (span != null)
{
Expand Down Expand Up @@ -505,11 +505,6 @@ private async Task UpdateChart(bool tickUpdate, DateTimeOffset inProgressDataTim
await OnChartUpdated(traces, xValues, exemplars, tickUpdate, inProgressDataTime);
}

protected OtlpSpan? GetSpan(string traceId, string spanId)
{
return MetricsHelpers.GetSpan(TelemetryRepository, traceId, spanId);
}

private DateTimeOffset GetCurrentDataTime()
{
return TimeProvider.GetUtcNow().Subtract(TimeSpan.FromSeconds(1)); // Compensate for delay in receiving metrics from services.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public ChartInterop(PlotlyChart plotlyChart)
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}

[JSInvokable]
Expand All @@ -207,7 +208,7 @@ public async Task ViewSpan(string traceId, string spanId)
var available = await MetricsHelpers.WaitForSpanToBeAvailableAsync(
traceId,
spanId,
_plotlyChart.GetSpan,
_plotlyChart.TelemetryRepository.GetSpan,
_plotlyChart.DialogService,
_plotlyChart.InvokeAsync,
_plotlyChart.DialogsLoc,
Expand Down
72 changes: 70 additions & 2 deletions src/Aspire.Dashboard/Components/Controls/SpanDetails.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Model.Otlp
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@inject IStringLocalizer<ControlsStrings> Loc
@inject IStringLocalizer<Dialogs> DialogsLoc

<div class="span-details-layout">
<FluentToolbar Orientation="Orientation.Horizontal">
Expand Down Expand Up @@ -91,8 +93,6 @@
<ExtraValueContent>
@if (context.Attributes.Length > 0)
{
<div class="event-attributes-header">@Loc[nameof(ControlsStrings.TraceDetailAttributesHeader)]</div>

/* There's a weird bug where trying to render a nested FluentDataGrid here with a non-primitive
TItem leads to the click event not being raised *only in the value (not header) rows*. A
workaround is to use the attribute index as the item and query the attributes list for its
Expand All @@ -109,6 +109,74 @@
</ExtraValueContent>
</PropertyGrid>
</FluentAccordionItem>
<FluentAccordionItem Heading="@Loc[nameof(ControlsStrings.SpanDetailsLinksHeader)]" Expanded="true">
<div slot="end">
<FluentBadge Appearance="Appearance.Neutral" Circular="true">
@FilteredSpanLinks.Count()
</FluentBadge>
</div>
<FluentDataGrid TGridItem="SpanLinkViewModel"
Items="@FilteredSpanLinks"
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="4fr 1fr">
<TemplateColumn Title="@Loc[nameof(ControlsStrings.SpanDetailsSpanColumnHeader)]">
@WriteSpanLink(context)
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(ControlsStrings.SpanDetailsDetailsColumnHeader)]">
<FluentButton Appearance="Appearance.Lightweight" OnClick="@(() => OnViewDetailsAsync(context))">@Loc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
<FluentAccordionItem Heading="@Loc[nameof(ControlsStrings.SpanDetailsBacklinksHeader)]" Expanded="true">
<div slot="end">
<FluentBadge Appearance="Appearance.Neutral" Circular="true">
@FilteredSpanBacklinks.Count()
</FluentBadge>
</div>
<FluentDataGrid TGridItem="SpanLinkViewModel"
Items="@FilteredSpanBacklinks"
Style="width:100%"
GenerateHeader="GenerateHeaderOption.Sticky"
GridTemplateColumns="4fr 1fr">
<TemplateColumn Title="@Loc[nameof(ControlsStrings.SpanDetailsSpanColumnHeader)]">
@WriteSpanLink(context)
</TemplateColumn>
<TemplateColumn Title="@Loc[nameof(ControlsStrings.SpanDetailsDetailsColumnHeader)]">
<FluentButton Appearance="Appearance.Lightweight" OnClick="@(() => OnViewDetailsAsync(context))">@Loc[nameof(ControlsStrings.ViewAction)]</FluentButton>
</TemplateColumn>
</FluentDataGrid>
</FluentAccordionItem>
</FluentAccordion>
</div>
</div>

@code {
RenderFragment WriteSpanLink(SpanLinkViewModel context)
{
var content = context.Span != null
? SpanWaterfallViewModel.GetTitle(context.Span, ViewModel.Applications)
: $"{Loc[nameof(ControlsStrings.SpanDetailsSpanPrefix)]}: {OtlpHelpers.ToShortenedId(context.SpanId)}";
var color = context.Span != null
? ColorGenerator.Instance.GetColorHexByKey(OtlpApplication.GetResourceName(context.Span.Source, ViewModel.Applications))
: "transparent";

return @<div>
<div class="spanlink-container" style="height:32px;">
<span class="spanlink-text" title="@content" style="padding-left:5px; border-left-width: 5px; border-left-style: solid; border-left-color: @(color);">
@content
</span>
</div>
@if (context.Attributes.Length > 0)
{
<PropertyGrid TItem="SpanPropertyViewModel"
Items="@Queryable.AsQueryable(context.Attributes.Select(a => new SpanPropertyViewModel() { Name = a.Key, Value = a.Value }))"
GridTemplateColumns="1fr 2fr"
NameColumnValue="(vm) => vm.Name"
ValueColumnValue="(vm) => vm.Value"
IsNameSortable="false"
IsValueSortable="false" />
}
</div>;
}
}
43 changes: 42 additions & 1 deletion src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@

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

namespace Aspire.Dashboard.Components.Controls;

public partial class SpanDetails
public partial class SpanDetails : IDisposable
{
[Parameter, EditorRequired]
public required SpanDetailsViewModel ViewModel { get; set; }

[Inject]
public required IDialogService DialogService { get; init; }

[Inject]
public required NavigationManager NavigationManager { get; init; }

[Inject]
public required TelemetryRepository TelemetryRepository { get; init; }

private IQueryable<SpanPropertyViewModel> FilteredItems =>
ViewModel.Properties.Where(ApplyFilter).AsQueryable();

Expand All @@ -27,8 +38,15 @@ public partial class SpanDetails
private IQueryable<OtlpSpanEvent> FilteredSpanEvents =>
ViewModel.Span.Events.Where(e => e.Name.Contains(_filter, StringComparison.CurrentCultureIgnoreCase)).OrderBy(e => e.Time).AsQueryable();

private IQueryable<SpanLinkViewModel> FilteredSpanLinks =>
ViewModel.Links.Where(e => e.SpanId.Contains(_filter, StringComparison.CurrentCultureIgnoreCase)).AsQueryable();

private IQueryable<SpanLinkViewModel> FilteredSpanBacklinks =>
ViewModel.Backlinks.Where(e => e.SpanId.Contains(_filter, StringComparison.CurrentCultureIgnoreCase)).AsQueryable();

private string _filter = "";
private List<KeyValuePair<string, string>> _contextAttributes = null!;
private readonly CancellationTokenSource _cts = new();

private readonly GridSort<SpanPropertyViewModel> _nameSort = GridSort<SpanPropertyViewModel>.ByAscending(vm => vm.Name);
private readonly GridSort<SpanPropertyViewModel> _valueSort = GridSort<SpanPropertyViewModel>.ByAscending(vm => vm.Value);
Expand Down Expand Up @@ -58,4 +76,27 @@ protected override void OnParametersSet()
_contextAttributes.Add(new KeyValuePair<string, string>("TraceId", ViewModel.Span.TraceId));
}
}

public async Task OnViewDetailsAsync(SpanLinkViewModel linkVM)
{
var available = await MetricsHelpers.WaitForSpanToBeAvailableAsync(
traceId: linkVM.TraceId,
spanId: linkVM.SpanId,
getSpan: TelemetryRepository.GetSpan,
DialogService,
InvokeAsync,
DialogsLoc,
_cts.Token).ConfigureAwait(false);

if (available)
{
NavigationManager.NavigateTo(DashboardUrls.TraceDetailUrl(linkVM.TraceId, spanId: linkVM.SpanId));
}
}

public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
13 changes: 10 additions & 3 deletions src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@
margin-bottom: 0;
}

::deep .event-attributes-header {
font-weight: bold;
margin: 5px 0;
::deep .spanlink-container {
display: grid;
grid-template-columns: 1fr auto;
gap: calc((6 + (var(--design-unit) * var(--density))) * 1px);
align-items: center;
}

::deep .spanlink-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task OnViewDetailsAsync(ChartExemplar exemplar)
var available = await MetricsHelpers.WaitForSpanToBeAvailableAsync(
traceId: exemplar.TraceId,
spanId: exemplar.SpanId,
getSpan: (traceId, spanId) => MetricsHelpers.GetSpan(TelemetryRepository, traceId, spanId),
getSpan: TelemetryRepository.GetSpan,
DialogService,
InvokeAsync,
Loc,
Expand Down Expand Up @@ -79,6 +79,7 @@ private string FormatMetricValue(double? value)

public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
Loading