Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
61 changes: 38 additions & 23 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@

namespace Aspire.Dashboard.Components.Pages;

public partial class TraceDetail
public partial class TraceDetail : ComponentBase
{
private readonly List<IDisposable> _peerChangesSubscriptions = new();
private OtlpTrace? _trace;
private OtlpSpan? _span;
private Subscription? _tracesSubscription;
private IDisposable? _peerChangesSubscription;
private List<SpanWaterfallViewModel>? _spanWaterfallViewModels;
private int _maxDepth;

Expand All @@ -30,15 +30,18 @@ public partial class TraceDetail
public required TelemetryRepository TelemetryRepository { get; set; }

[Inject]
public required IOutgoingPeerResolver OutgoingPeerResolver { get; set; }
public required IEnumerable<IOutgoingPeerResolver> OutgoingPeerResolvers { get; set; }

protected override void OnInitialized()
{
_peerChangesSubscription = OutgoingPeerResolver.OnPeerChanges(async () =>
foreach (var resolver in OutgoingPeerResolvers)
{
UpdateDetailViewData();
await InvokeAsync(StateHasChanged);
});
_peerChangesSubscriptions.Add(resolver.OnPeerChanges(async () =>
{
UpdateDetailViewData();
await InvokeAsync(StateHasChanged);
}));
}
}

private ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridItemsProviderRequest<SpanWaterfallViewModel> request)
Expand All @@ -52,34 +55,34 @@ private ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridI
});
}

private static List<SpanWaterfallViewModel> CreateSpanWaterfallViewModels(OtlpTrace trace, IOutgoingPeerResolver outgoingPeerResolver)
private static List<SpanWaterfallViewModel> CreateSpanWaterfallViewModels(OtlpTrace trace, IEnumerable<IOutgoingPeerResolver> outgoingPeerResolvers)
{
var orderedSpans = new List<SpanWaterfallViewModel>();
// There should be one root span but just in case, we'll add them all.
foreach (var rootSpan in trace.Spans.Where(s => string.IsNullOrEmpty(s.ParentSpanId)).OrderBy(s => s.StartTime))
{
AddSelfAndChildren(orderedSpans, rootSpan, depth: 1, outgoingPeerResolver, CreateViewModel);
AddSelfAndChildren(orderedSpans, rootSpan, depth: 1, outgoingPeerResolvers, CreateViewModel);
}
// Unparented spans.
foreach (var unparentedSpan in trace.Spans.Where(s => !string.IsNullOrEmpty(s.ParentSpanId) && s.GetParentSpan() == null).OrderBy(s => s.StartTime))
{
AddSelfAndChildren(orderedSpans, unparentedSpan, depth: 1, outgoingPeerResolver, CreateViewModel);
AddSelfAndChildren(orderedSpans, unparentedSpan, depth: 1, outgoingPeerResolvers, CreateViewModel);
}

return orderedSpans;

static void AddSelfAndChildren(List<SpanWaterfallViewModel> orderedSpans, OtlpSpan span, int depth, IOutgoingPeerResolver outgoingPeerResolver, Func<OtlpSpan, int, IOutgoingPeerResolver, SpanWaterfallViewModel> createViewModel)
static void AddSelfAndChildren(List<SpanWaterfallViewModel> orderedSpans, OtlpSpan span, int depth, IEnumerable<IOutgoingPeerResolver> outgoingPeerResolvers, Func<OtlpSpan, int, IEnumerable<IOutgoingPeerResolver>, SpanWaterfallViewModel> createViewModel)
{
orderedSpans.Add(createViewModel(span, depth, outgoingPeerResolver));
orderedSpans.Add(createViewModel(span, depth, outgoingPeerResolvers));
depth++;

foreach (var child in span.GetChildSpans().OrderBy(s => s.StartTime))
{
AddSelfAndChildren(orderedSpans, child, depth, outgoingPeerResolver, createViewModel);
AddSelfAndChildren(orderedSpans, child, depth, outgoingPeerResolvers, createViewModel);
}
}

static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, IOutgoingPeerResolver outgoingPeerResolver)
static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, IEnumerable<IOutgoingPeerResolver> outgoingPeerResolvers)
{
var traceStart = span.Trace.FirstSpan.StartTime;
var relativeStart = span.StartTime - traceStart;
Expand All @@ -95,13 +98,7 @@ static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, IOutgoin
// A span may indicate a call to another service but the service isn't instrumented.
var hasPeerService = span.Attributes.Any(a => a.Key == OtlpSpan.PeerServiceAttributeKey);
var isUninstrumentedPeer = hasPeerService && span.Kind is OtlpSpanKind.Client or OtlpSpanKind.Producer && !span.GetChildSpans().Any();
var uninstrumentedPeer = isUninstrumentedPeer
? OtlpHelpers.GetValue(span.Attributes, OtlpSpan.PeerServiceAttributeKey)
: null;
if (uninstrumentedPeer != null)
{
uninstrumentedPeer = outgoingPeerResolver.ResolvePeerName(uninstrumentedPeer);
}
var uninstrumentedPeer = isUninstrumentedPeer ? ResolveUninstrumentedPeerName(span, outgoingPeerResolvers) : null;

var viewModel = new SpanWaterfallViewModel
{
Expand All @@ -116,6 +113,21 @@ static SpanWaterfallViewModel CreateViewModel(OtlpSpan span, int depth, IOutgoin
}
}

private static string? ResolveUninstrumentedPeerName(OtlpSpan span, IEnumerable<IOutgoingPeerResolver> outgoingPeerResolvers)
{
// Attempt to resolve uninstrumented peer to a friendly name from the span.
foreach (var resolver in outgoingPeerResolvers)
{
if (resolver.TryResolvePeerName(span, out var name))
{
return name;
}
}

// Fallback to the peer address.
return OtlpHelpers.GetValue(span.Attributes, OtlpSpan.PeerServiceAttributeKey);
}

protected override void OnParametersSet()
{
UpdateDetailViewData();
Expand All @@ -131,7 +143,7 @@ private void UpdateDetailViewData()
_trace = TelemetryRepository.GetTrace(TraceId);
if (_trace != null)
{
_spanWaterfallViewModels = CreateSpanWaterfallViewModels(_trace, OutgoingPeerResolver);
_spanWaterfallViewModels = CreateSpanWaterfallViewModels(_trace, OutgoingPeerResolvers);
_maxDepth = _spanWaterfallViewModels.Max(s => s.Depth);

if (_tracesSubscription is null || _tracesSubscription.ApplicationId != _trace.FirstSpan.Source.InstanceId)
Expand Down Expand Up @@ -189,7 +201,10 @@ private void ClearSelectedSpan()

public void Dispose()
{
_peerChangesSubscription?.Dispose();
foreach (var subscription in _peerChangesSubscriptions)
{
subscription.Dispose();
}
_tracesSubscription?.Dispose();
}
}
4 changes: 3 additions & 1 deletion src/Aspire.Dashboard/DashboardWebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.FluentUI.AspNetCore.Components;
Expand Down Expand Up @@ -86,9 +87,10 @@ public DashboardWebApplication(ILogger<DashboardWebApplication> logger, Action<I
// OTLP services.
builder.Services.AddGrpc();
builder.Services.AddSingleton<TelemetryRepository>();
builder.Services.AddScoped<IOutgoingPeerResolver, ResourceOutgoingPeerResolver>();
builder.Services.AddTransient<StructuredLogsViewModel>();
builder.Services.AddTransient<TracesViewModel>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<IOutgoingPeerResolver, ResourceOutgoingPeerResolver>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<IOutgoingPeerResolver, BrowserLinkOutgoingPeerResolver>());

builder.Services.AddFluentUIComponents();

Expand Down
55 changes: 55 additions & 0 deletions src/Aspire.Dashboard/Model/BrowserLinkOutgoingPeerResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Otlp.Model;

namespace Aspire.Dashboard.Model;

public sealed class BrowserLinkOutgoingPeerResolver : IOutgoingPeerResolver
{
public IDisposable OnPeerChanges(Func<Task> callback)
{
return new NullSubscription();
}

private sealed class NullSubscription : IDisposable
{
public void Dispose()
{
}
}

public bool TryResolvePeerName(OtlpSpan span, [NotNullWhen(true)] out string? name)
{
// There isn't a good way to identify the HTTP request the BrowserLink middleware makes to
// the IDE to get the script tag. The logic below looks at the host and URL and identifies
// the HTTP request by its shape.
// Example URL: http://localhost:59267/6eed7c2dedc14419901b813e8fe87a86/getScriptTag
//
// There is the chance future BrowserLink changes make this detection invalid.
// Also, it's possible to misidentify a HTTP request.
//
// A long term improvement here is to add tags to the BrowserLink client and then detect the
// values in the span's attributes.
var url = OtlpHelpers.GetValue(span.Attributes, "http.url");
if (url != null && Uri.TryCreate(url, UriKind.Absolute, out var uri))
Comment thread
JamesNK marked this conversation as resolved.
Outdated
{
if (string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{
var parts = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
if (Guid.TryParse(parts[0], out _) && string.Equals(parts[1], "getScriptTag", StringComparison.OrdinalIgnoreCase))
{
name = "browserlink";
Comment thread
JamesNK marked this conversation as resolved.
Outdated
return true;
}
}
}
}

name = null;
return false;
}
}
4 changes: 3 additions & 1 deletion src/Aspire.Dashboard/Model/IOutgoingPeerResolver.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// 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.Otlp.Model;

namespace Aspire.Dashboard.Model;

public interface IOutgoingPeerResolver
{
string ResolvePeerName(string networkAddress);
bool TryResolvePeerName(OtlpSpan span, out string? name);
IDisposable OnPeerChanges(Func<Task> callback);
}
20 changes: 14 additions & 6 deletions src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Otlp.Model;

namespace Aspire.Dashboard.Model;

Expand Down Expand Up @@ -55,20 +57,26 @@ private async Task OnResourceListChanged(ObjectChangeType changeType, ResourceVi
await RaisePeerChangesAsync().ConfigureAwait(false);
}

public string ResolvePeerName(string networkAddress)
public bool TryResolvePeerName(OtlpSpan span, [NotNullWhen(true)] out string? name)
{
foreach (var (resourceName, resource) in _resourceNameMapping)
var address = OtlpHelpers.GetValue(span.Attributes, OtlpSpan.PeerServiceAttributeKey);
if (address != null)
{
foreach (var service in resource.Services)
foreach (var (resourceName, resource) in _resourceNameMapping)
{
if (string.Equals(service.AddressAndPort, networkAddress, StringComparison.OrdinalIgnoreCase))
foreach (var service in resource.Services)
{
return resource.Name;
if (string.Equals(service.AddressAndPort, address, StringComparison.OrdinalIgnoreCase))
{
name = resource.Name;
return true;
}
}
}
}

return networkAddress;
name = null;
return false;
}

public IDisposable OnPeerChanges(Func<Task> callback)
Expand Down