Skip to content
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
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
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