Skip to content

Commit

Permalink
Support displaying different resource properties (#5526)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Sep 10, 2024
1 parent 748ffcf commit 47ffa3b
Show file tree
Hide file tree
Showing 24 changed files with 364 additions and 105 deletions.
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Controls/SpanDetails.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<div class="span-details-layout">
<FluentToolbar Orientation="Orientation.Horizontal">
<div>
@((MarkupString)string.Format(ControlsStrings.SpanDetailsResource, ViewModel.Span.Source.ApplicationName))
@((MarkupString)string.Format(ControlsStrings.SpanDetailsResource, ViewModel.Span.Source.Application.ApplicationName))
</div>
<FluentDivider Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="structured-log-details-layout">
<FluentToolbar Orientation="Orientation.Horizontal">
<div>
@((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsResource, ViewModel.LogEntry.Application.ApplicationName))
@((MarkupString)string.Format(ControlsStrings.StructuredLogsDetailsResource, ViewModel.LogEntry.ApplicationView.Application.ApplicationName))
</div>
<FluentDivider Role="DividerRole.Presentation" Orientation="Orientation.Vertical" />
<div title="@FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, ViewModel.LogEntry.TimeStamp, MillisecondsDisplay.Full)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public partial class StructuredLogDetails
.Where(ApplyFilter).AsQueryable();

private IQueryable<LogEntryPropertyViewModel> FilteredResourceItems =>
ViewModel.LogEntry.Application.AllProperties().Select(p => new LogEntryPropertyViewModel { Name = p.Key, Value = p.Value })
ViewModel.LogEntry.ApplicationView.AllProperties().Select(p => new LogEntryPropertyViewModel { Name = p.Key, Value = p.Value })
.Where(ApplyFilter).AsQueryable();

private string _filter = "";
Expand Down
5 changes: 2 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Aspire.Dashboard.Extensions;

using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Utils;
Expand All @@ -31,7 +30,7 @@ public partial class Resources : ComponentBase, IAsyncDisposable

private Subscription? _logsSubscription;
private IList<GridColumn>? _gridColumns;
private Dictionary<OtlpApplication, int>? _applicationUnviewedErrorCounts;
private Dictionary<ApplicationKey, int>? _applicationUnviewedErrorCounts;

[Inject]
public required IDashboardClient DashboardClient { get; init; }
Expand Down Expand Up @@ -224,7 +223,7 @@ async Task SubscribeResourcesAsync()
}
}

private bool ApplicationErrorCountsChanged(Dictionary<OtlpApplication, int> newApplicationUnviewedErrorCounts)
private bool ApplicationErrorCountsChanged(Dictionary<ApplicationKey, int> newApplicationUnviewedErrorCounts)
{
if (_applicationUnviewedErrorCounts == null || _applicationUnviewedErrorCounts.Count != newApplicationUnviewedErrorCounts.Count)
{
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@
OnRowClick="@(r => r.ExecuteOnDefault(d => OnShowPropertiesAsync(d, buttonId: null)))"
Class="enable-row-click">
<ChildContent>
<AspireTemplateColumn ColumnId="@ResourceColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsResourceColumnHeader)]" Tooltip="true" TooltipText="@(e => GetResourceName(e.Application))">
<span style="padding-left:5px; border-left-width: 5px; border-left-style: solid; border-left-color: @(ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Application)));">
@GetResourceName(context.Application)
<AspireTemplateColumn ColumnId="@ResourceColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsResourceColumnHeader)]" Tooltip="true" TooltipText="@(e => GetResourceName(e.ApplicationView))">
<span style="padding-left:5px; border-left-width: 5px; border-left-style: solid; border-left-color: @(ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.ApplicationView)));">
@GetResourceName(context.ApplicationView)
</span>
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@LogLevelColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsLevelColumnHeader)]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private async Task HandleAfterFilterBindAsync()
await this.AfterViewModelChangedAsync(_contentLayout, true);
}

private string GetResourceName(OtlpApplication app) => OtlpApplication.GetResourceName(app, _applications);
private string GetResourceName(OtlpApplicationView app) => OtlpApplication.GetResourceName(app.Application, _applications);

private string GetRowClass(OtlpLogEntry entry)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)
_elementIdBeforeDetailsViewOpened = null;
}

private string GetResourceName(OtlpApplication app) => OtlpApplication.GetResourceName(app, _applications);
private string GetResourceName(OtlpApplicationView app) => OtlpApplication.GetResourceName(app, _applications);

public void Dispose()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Traces.razor
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<AspireTemplateColumn ColumnId="@SpansColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.Traces.TracesSpansColumnHeader)]">
<FluentOverflow>
<ChildContent>
@foreach (var item in context.Spans.GroupBy(s => s.Source).OrderBy(g => g.Min(s => s.StartTime)))
@foreach (var item in context.Spans.GroupBy(s => s.Source.Application).OrderBy(g => g.Min(s => s.StartTime)))
{
<FluentOverflowItem>
<span class="trace-tag trace-service-tag" title="@(GetSpansTooltip(item))" style="border-left-color: @(ColorGenerator.Instance.GetColorHexByKey(GetResourceName(item.Key)));">
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Dashboard/Components/Pages/Traces.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ private async Task HandleAfterFilterBindAsync()
}

private string GetResourceName(OtlpApplication app) => OtlpApplication.GetResourceName(app, _applications);
private string GetResourceName(OtlpApplicationView app) => OtlpApplication.GetResourceName(app, _applications);

protected override async Task OnAfterRenderAsync(bool firstRender)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@using Aspire.Dashboard.Extensions
@using Aspire.Dashboard.Model
@using Aspire.Dashboard.Otlp.Model
@using Aspire.Dashboard.Otlp.Storage
@using Aspire.Dashboard.Resources
@using Humanizer

Expand Down Expand Up @@ -99,5 +100,5 @@ else


[Parameter, EditorRequired]
public required Dictionary<OtlpApplication, int>? UnviewedErrorCounts { get; set; }
public required Dictionary<ApplicationKey, int>? UnviewedErrorCounts { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
Expand All @@ -17,7 +16,7 @@ public partial class UnreadLogErrorsBadge
[Parameter, EditorRequired]
public required ResourceViewModel Resource { get; set; }
[Parameter, EditorRequired]
public required Dictionary<OtlpApplication, int>? UnviewedErrorCounts { get; set; }
public required Dictionary<ApplicationKey, int>? UnviewedErrorCounts { get; set; }

[Inject]
public required TelemetryRepository TelemetryRepository { get; init; }
Expand All @@ -42,7 +41,7 @@ protected override void OnParametersSet()
return (null, 0);
}

if (!UnviewedErrorCounts.TryGetValue(application, out var count) || count == 0)
if (!UnviewedErrorCounts.TryGetValue(application.ApplicationKey, out var count) || count == 0)
{
return (null, 0);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Extensions/FluentUIExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static Dictionary<string, object> GetClipboardCopyAdditionalAttributes(st
// No onclick attribute is added here. The CSP restricts inline scripts, including onclick.
// Instead, a click event listener is added to the document and clicking the button is bubbled up to the event.
// The document click listener looks for a button element and these attributes.
var attributes = new Dictionary<string, object>(StringComparers.Attribute)
var attributes = new Dictionary<string, object>(StringComparers.HtmlAttribute)
{
{ "data-text", text ?? string.Empty },
{ "data-precopy", precopy ?? string.Empty },
Expand All @@ -28,7 +28,7 @@ public static Dictionary<string, object> GetClipboardCopyAdditionalAttributes(st

public static Dictionary<string, object> GetOpenTextVisualizerAdditionalAttributes(string textValue, string textValueDescription, params (string Attribute, object Value)[] additionalAttributes)
{
var attributes = new Dictionary<string, object>(StringComparers.Attribute)
var attributes = new Dictionary<string, object>(StringComparers.HtmlAttribute)
{
{ "data-text", textValue },
{ "data-textvisualizer-description", textValueDescription }
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Model/Otlp/LogFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ private static Func<double, double, bool> ConditionToFuncNumber(FilterCondition
return Field switch
{
KnownMessageField => x.Message,
KnownApplicationField => x.Application.ApplicationName,
KnownApplicationField => x.ApplicationView.Application.ApplicationName,
KnownTraceIdField => x.TraceId,
KnownSpanIdField => x.SpanId,
KnownOriginalFormatField => x.OriginalFormat,
Expand Down
107 changes: 72 additions & 35 deletions src/Aspire.Dashboard/Otlp/Model/OtlpApplication.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Otlp.Storage;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Metrics.V1;
using OpenTelemetry.Proto.Resource.V1;

namespace Aspire.Dashboard.Otlp.Model;

Expand All @@ -26,52 +27,20 @@ public class OtlpApplication
private readonly ReaderWriterLockSlim _metricsLock = new();
private readonly Dictionary<string, OtlpMeter> _meters = new();
private readonly Dictionary<OtlpInstrumentKey, OtlpInstrument> _instruments = new();
private readonly ConcurrentDictionary<KeyValuePair<string, string>[], OtlpApplicationView> _applicationViews = new(ApplicationViewKeyComparer.Instance);

private readonly ILogger _logger;
private readonly TelemetryLimitOptions _options;

public KeyValuePair<string, string>[] Properties { get; }

public OtlpApplication(string name, string instanceId, Resource resource, ILogger logger, TelemetryLimitOptions options)
public OtlpApplication(string name, string instanceId, ILogger logger, TelemetryLimitOptions options)
{
var properties = new List<KeyValuePair<string, string>>();
foreach (var attribute in resource.Attributes)
{
switch (attribute.Key)
{
case SERVICE_NAME:
case SERVICE_INSTANCE_ID:
// Values passed in via ctor and set to members. Don't add to properties collection.
break;
default:
properties.Add(new KeyValuePair<string, string>(attribute.Key, attribute.Value.GetString()));
break;

}
}
Properties = properties.ToArray();

ApplicationName = name;
InstanceId = instanceId;

_logger = logger;
_options = options;
}

public Dictionary<string, string> AllProperties()
{
var props = new Dictionary<string, string>();
props.Add(SERVICE_NAME, ApplicationName);
props.Add(SERVICE_INSTANCE_ID, InstanceId);

foreach (var kv in Properties)
{
props.TryAdd(kv.Key, kv.Value);
}

return props;
}

public void AddMetrics(AddContext context, RepeatedField<ScopeMetrics> scopeMetrics)
{
_metricsLock.EnterWriteLock();
Expand Down Expand Up @@ -185,6 +154,9 @@ public static Dictionary<string, List<OtlpApplication>> GetReplicasByApplication
.ToDictionary(grouping => grouping.Key, grouping => grouping.ToList());
}

public static string GetResourceName(OtlpApplicationView app, List<OtlpApplication> allApplications) =>
GetResourceName(app.Application, allApplications);

public static string GetResourceName(OtlpApplication app, List<OtlpApplication> allApplications)
{
var count = 0;
Expand Down Expand Up @@ -216,4 +188,69 @@ public static string GetResourceName(OtlpApplication app, List<OtlpApplication>

return app.ApplicationName;
}

internal List<OtlpApplicationView> GetViews() => _applicationViews.Values.ToList();

internal OtlpApplicationView GetView(RepeatedField<KeyValue> attributes)
{
// Inefficient to create this to possibly throw it away.
var view = new OtlpApplicationView(this, attributes);

if (_applicationViews.TryGetValue(view.Properties, out var applicationView))
{
return applicationView;
}

return _applicationViews.GetOrAdd(view.Properties, view);
}

/// <summary>
/// Application views are equal when all properties are equal.
/// </summary>
private sealed class ApplicationViewKeyComparer : IEqualityComparer<KeyValuePair<string, string>[]>
{
public static readonly ApplicationViewKeyComparer Instance = new();

public bool Equals(KeyValuePair<string, string>[]? x, KeyValuePair<string, string>[]? y)
{
if (x == y)
{
return true;
}
if (x == null || y == null)
{
return false;
}
if (x.Length != y.Length)
{
return false;
}

for (var i = 0; i < x.Length; i++)
{
if (!string.Equals(x[i].Key, y[i].Key, StringComparisons.OtlpAttribute))
{
return false;
}
if (!string.Equals(x[i].Value, y[i].Value, StringComparisons.OtlpAttribute))
{
return false;
}
}

return true;
}

public int GetHashCode([DisallowNull] KeyValuePair<string, string>[] obj)
{
var hashCode = new HashCode();
for (var i = 0; i < obj.Length; i++)
{
hashCode.Add(StringComparers.OtlpAttribute.GetHashCode(obj[i].Key));
hashCode.Add(StringComparers.OtlpAttribute.GetHashCode(obj[i].Value));
}

return hashCode.ToHashCode();
}
}
}
66 changes: 66 additions & 0 deletions src/Aspire.Dashboard/Otlp/Model/OtlpApplicationView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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;
using Aspire.Dashboard.Otlp.Storage;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Common.V1;

namespace Aspire.Dashboard.Otlp.Model;

[DebuggerDisplay("Application = {Application}, Properties = {Properties.Count}")]
public class OtlpApplicationView
{
public ApplicationKey ApplicationKey => Application.ApplicationKey;
public OtlpApplication Application { get; }
public KeyValuePair<string, string>[] Properties { get; }

public OtlpApplicationView(OtlpApplication application, RepeatedField<KeyValue> attributes)
{
Application = application;

List<KeyValuePair<string, string>>? properties = null;
foreach (var attribute in attributes)
{
switch (attribute.Key)
{
case OtlpApplication.SERVICE_NAME:
case OtlpApplication.SERVICE_INSTANCE_ID:
// Values passed in via ctor and set to members. Don't add to properties collection.
break;
default:
properties ??= [];
properties.Add(new KeyValuePair<string, string>(attribute.Key, attribute.Value.GetString()));
break;

}
}

if (properties != null)
{
// Sort so keys are in a consistent order for equality check.
properties.Sort((p1, p2) => string.Compare(p1.Key, p2.Key, StringComparisons.OtlpAttribute));
Properties = properties.ToArray();
}
else
{
Properties = [];
}
}

public Dictionary<string, string> AllProperties()
{
var props = new Dictionary<string, string>(StringComparers.OtlpAttribute)
{
{ OtlpApplication.SERVICE_NAME, Application.ApplicationName },
{ OtlpApplication.SERVICE_INSTANCE_ID, Application.InstanceId }
};

foreach (var kv in Properties)
{
props.TryAdd(kv.Key, kv.Value);
}

return props;
}
}
Loading

0 comments on commit 47ffa3b

Please sign in to comment.