Skip to content

Commit bb5fb93

Browse files
authored
Minor dashboard improvements and clean up (#9301)
1 parent eefa50f commit bb5fb93

29 files changed

+412
-167
lines changed

src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,9 @@ public partial class SettingsDialog : IDialogContentComponent, IDisposable
3939

4040
protected override void OnInitialized()
4141
{
42-
// Order cultures in the dropdown with invariant culture. This prevents the order of languages changing when the culture changes.
43-
_languageOptions = [.. GlobalizationHelpers.LocalizedCultures.OrderBy(c => c.NativeName, StringComparer.InvariantCultureIgnoreCase)];
42+
_languageOptions = GlobalizationHelpers.OrderedLocalizedCultures;
4443

45-
_selectedUiCulture = GlobalizationHelpers.TryGetKnownParentCulture(_languageOptions, CultureInfo.CurrentUICulture, out var matchedCulture)
44+
_selectedUiCulture = GlobalizationHelpers.TryGetKnownParentCulture(CultureInfo.CurrentUICulture, out var matchedCulture)
4645
? matchedCulture :
4746
// Otherwise, Blazor has fallen back to a supported language
4847
CultureInfo.CurrentUICulture;

src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,6 @@ public partial class LogMessageColumnDisplay
1111

1212
protected override void OnInitialized()
1313
{
14-
_exceptionText = GetExceptionText();
15-
}
16-
17-
private string? GetExceptionText()
18-
{
19-
// exception.stacktrace includes the exception message and type.
20-
// https://opentelemetry.io/docs/specs/semconv/attributes-registry/exception/
21-
if (GetProperty("exception.stacktrace") is { Length: > 0 } stackTrace)
22-
{
23-
return stackTrace;
24-
}
25-
26-
if (GetProperty("exception.message") is { Length: > 0 } message)
27-
{
28-
if (GetProperty("exception.type") is { Length: > 0 } type)
29-
{
30-
return $"{type}: {message}";
31-
}
32-
33-
return message;
34-
}
35-
36-
return null;
37-
38-
string? GetProperty(string propertyName)
39-
{
40-
return LogEntry.Attributes.GetValue(propertyName);
41-
}
14+
_exceptionText = OtlpLogEntry.GetExceptionText(LogEntry);
4215
}
4316
}

src/Aspire.Dashboard/DashboardEndpointsBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.Si
5252
}
5353

5454
// The passed in language should be one of the localized cultures.
55-
var newLanguage = GlobalizationHelpers.LocalizedCultures.SingleOrDefault(c => string.Equals(c.Name, language, StringComparisons.CultureName));
55+
var newLanguage = GlobalizationHelpers.OrderedLocalizedCultures.SingleOrDefault(c => string.Equals(c.Name, language, StringComparisons.CultureName));
5656
if (newLanguage == null)
5757
{
5858
return Results.BadRequest();

src/Aspire.Dashboard/Extensions/ResourceViewModelExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public static bool IsNotStarted(this ResourceViewModel resource)
4242
return resource.KnownState is KnownResourceState.NotStarted;
4343
}
4444

45+
public static bool IsWaiting(this ResourceViewModel resource)
46+
{
47+
return resource.KnownState is KnownResourceState.Waiting;
48+
}
49+
4550
public static bool IsUnknownState(this ResourceViewModel resource) => resource.KnownState is KnownResourceState.Unknown;
4651

4752
public static bool HasNoState(this ResourceViewModel resource) => string.IsNullOrEmpty(resource.State);

src/Aspire.Dashboard/Model/DebugSessionHelpers.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
@@ -10,7 +10,7 @@ namespace Aspire.Dashboard.Model;
1010

1111
internal static class DebugSessionHelpers
1212
{
13-
public static HttpClient CreateHttpClient(Uri debugSessionUri, string token, X509Certificate2? cert, Func<HttpClientHandler, HttpMessageHandler>? createHandler)
13+
public static HttpClient CreateHttpClient(Uri? debugSessionUri, string? token, X509Certificate2? cert, Func<HttpClientHandler, HttpMessageHandler>? createHandler)
1414
{
1515
var handler = new HttpClientHandler();
1616
if (cert is not null)
@@ -23,23 +23,34 @@ public static HttpClient CreateHttpClient(Uri debugSessionUri, string token, X50
2323
return true;
2424
}
2525

26+
if (c == null)
27+
{
28+
return false;
29+
}
30+
2631
// Certificate isn't immediately valid. Check if it is the same as the one we expect.
2732
// It's ok that comparison isn't time constant because this is public information.
28-
return cert.RawData.SequenceEqual(c?.RawData);
33+
return cert.RawData.SequenceEqual(c.RawData);
2934
};
3035
}
3136

3237
var resolvedHandler = createHandler?.Invoke(handler) ?? handler;
3338
var client = new HttpClient(resolvedHandler)
3439
{
35-
BaseAddress = debugSessionUri,
36-
DefaultRequestHeaders =
37-
{
38-
{ "Authorization", $"Bearer {token}" },
39-
{ "User-Agent", "Aspire Dashboard" }
40-
}
40+
Timeout = Timeout.InfiniteTimeSpan
4141
};
4242

43+
if (debugSessionUri is not null)
44+
{
45+
client.BaseAddress = debugSessionUri;
46+
}
47+
48+
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Aspire Dashboard");
49+
if (token != null)
50+
{
51+
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");
52+
}
53+
4354
return client;
4455
}
4556

src/Aspire.Dashboard/Model/ResourceStateViewModel.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ internal static string GetResourceStateTooltip(ResourceViewModel resource, IStri
126126
// DCP reports the container runtime is unhealthy. Most likely the container runtime (e.g. Docker) isn't running.
127127
return loc[nameof(Columns.StateColumnResourceContainerRuntimeUnhealthy)];
128128
}
129+
else if (resource.IsWaiting())
130+
{
131+
return loc[nameof(Columns.StateColumnResourceWaiting)];
132+
}
133+
else if (resource.IsNotStarted())
134+
{
135+
return loc[nameof(Columns.StateColumnResourceNotStarted)];
136+
}
129137

130138
// Fallback to text displayed in column.
131139
return GetStateText(resource, loc);

src/Aspire.Dashboard/Model/ResourceViewModel.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,14 @@ public bool IsResourceHidden()
103103
}
104104

105105
public static string GetResourceName(ResourceViewModel resource, IDictionary<string, ResourceViewModel> allResources)
106+
{
107+
return GetResourceName(resource, allResources.Values);
108+
}
109+
110+
public static string GetResourceName(ResourceViewModel resource, IEnumerable<ResourceViewModel> allResources)
106111
{
107112
var count = 0;
108-
foreach (var (_, item) in allResources)
113+
foreach (var item in allResources)
109114
{
110115
if (item.IsResourceHidden())
111116
{

src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public static class OtlpHelpers
2424
WriteIndented = false
2525
};
2626

27+
public const int ShortenedIdLength = 7;
28+
2729
public static ApplicationKey GetApplicationKey(this Resource resource)
2830
{
2931
string? serviceName = null;
@@ -62,7 +64,7 @@ public static ApplicationKey GetApplicationKey(this Resource resource)
6264
return new ApplicationKey(serviceName, serviceInstanceId ?? serviceName);
6365
}
6466

65-
public static string ToShortenedId(string id) => TruncateString(id, maxLength: 7);
67+
public static string ToShortenedId(string id) => TruncateString(id, maxLength: ShortenedIdLength);
6668

6769
public static string ToHexString(ReadOnlyMemory<byte> bytes)
6870
{
@@ -429,6 +431,20 @@ public static PagedResult<TResult> GetItems<TSource, TResult>(IEnumerable<TSourc
429431
};
430432
}
431433

434+
public static bool MatchTelemetryId(string incomingId, string existingId)
435+
{
436+
// This method uses StartsWith to find a match.
437+
// We only want to use that logic if the traceId is at least the length of a shortened id.
438+
if (incomingId.Length >= ShortenedIdLength)
439+
{
440+
return existingId.StartsWith(incomingId, StringComparison.OrdinalIgnoreCase);
441+
}
442+
else
443+
{
444+
return existingId.Equals(incomingId, StringComparison.OrdinalIgnoreCase);
445+
}
446+
}
447+
432448
public static bool TryAddScope(Dictionary<string, OtlpScope> scopes, InstrumentationScope? scope, OtlpContext context, [NotNullWhen(true)] out OtlpScope? s)
433449
{
434450
try

src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
namespace Aspire.Dashboard.Otlp.Model;
99

10-
[DebuggerDisplay("TimeStamp = {TimeStamp}, Severity = {Severity}, Message = {Message}")]
10+
[DebuggerDisplay("InternalId = {InternalId}, TimeStamp = {TimeStamp}, Severity = {Severity}, Message = {Message}")]
1111
public class OtlpLogEntry
1212
{
13+
private static long s_nextLogEntryId;
14+
1315
public KeyValuePair<string, string>[] Attributes { get; }
1416
public DateTime TimeStamp { get; }
1517
public uint Flags { get; }
@@ -21,11 +23,11 @@ public class OtlpLogEntry
2123
public string? OriginalFormat { get; }
2224
public OtlpApplicationView ApplicationView { get; }
2325
public OtlpScope Scope { get; }
24-
public Guid InternalId { get; }
26+
public long InternalId { get; }
2527

2628
public OtlpLogEntry(LogRecord record, OtlpApplicationView logApp, OtlpScope scope, OtlpContext context)
2729
{
28-
InternalId = Guid.NewGuid();
30+
InternalId = Interlocked.Increment(ref s_nextLogEntryId);
2931
TimeStamp = ResolveTimeStamp(record);
3032

3133
string? originalFormat = null;
@@ -118,4 +120,35 @@ private static DateTime ResolveTimeStamp(LogRecord record)
118120
_ => log.Attributes.GetValue(field)
119121
};
120122
}
123+
124+
public const string ExceptionStackTraceField = "exception.stacktrace";
125+
public const string ExceptionMessageField = "exception.message";
126+
public const string ExceptionTypeField = "exception.type";
127+
128+
public static string? GetExceptionText(OtlpLogEntry logEntry)
129+
{
130+
// exception.stacktrace includes the exception message and type.
131+
// https://opentelemetry.io/docs/specs/semconv/attributes-registry/exception/
132+
if (GetProperty(logEntry, ExceptionStackTraceField) is { Length: > 0 } stackTrace)
133+
{
134+
return stackTrace;
135+
}
136+
137+
if (GetProperty(logEntry, ExceptionMessageField) is { Length: > 0 } message)
138+
{
139+
if (GetProperty(logEntry, ExceptionTypeField) is { Length: > 0 } type)
140+
{
141+
return $"{type}: {message}";
142+
}
143+
144+
return message;
145+
}
146+
147+
return null;
148+
149+
static string? GetProperty(OtlpLogEntry logEntry, string propertyName)
150+
{
151+
return logEntry.Attributes.GetValue(propertyName);
152+
}
153+
}
121154
}

src/Aspire.Dashboard/Otlp/Storage/Subscription.cs

Lines changed: 5 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using Aspire.Dashboard.Utils;
56

67
namespace Aspire.Dashboard.Otlp.Storage;
78

@@ -10,18 +11,10 @@ public sealed class Subscription : IDisposable
1011
{
1112
private static int s_subscriptionId;
1213

13-
private readonly Func<Task> _callback;
14-
private readonly ExecutionContext? _executionContext;
15-
private readonly TelemetryRepository _telemetryRepository;
16-
private readonly CancellationTokenSource _cts;
17-
private readonly CancellationToken _cancellationToken;
14+
private readonly CallbackThrottler _callbackThrottler;
1815
private readonly Action _unsubscribe;
19-
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
20-
private ILogger Logger => _telemetryRepository._otlpContext.Logger;
2116
private readonly int _subscriptionId = Interlocked.Increment(ref s_subscriptionId);
2217

23-
private DateTime? _lastExecute;
24-
2518
public int SubscriptionId => _subscriptionId;
2619
public ApplicationKey? ApplicationKey { get; }
2720
public SubscriptionType SubscriptionType { get; }
@@ -32,87 +25,18 @@ public Subscription(string name, ApplicationKey? applicationKey, SubscriptionTyp
3225
Name = name;
3326
ApplicationKey = applicationKey;
3427
SubscriptionType = subscriptionType;
35-
_callback = callback;
28+
_callbackThrottler = new CallbackThrottler(name, telemetryRepository._otlpContext.Logger, telemetryRepository._subscriptionMinExecuteInterval, callback, executionContext);
3629
_unsubscribe = unsubscribe;
37-
_executionContext = executionContext;
38-
_telemetryRepository = telemetryRepository;
39-
_cts = new CancellationTokenSource();
40-
_cancellationToken = _cts.Token;
41-
}
42-
43-
private async Task<bool> TryQueueAsync(CancellationToken cancellationToken)
44-
{
45-
var success = _lock.Wait(0, cancellationToken);
46-
if (!success)
47-
{
48-
Logger.LogDebug("Subscription '{Name}' update already queued.", Name);
49-
return false;
50-
}
51-
52-
try
53-
{
54-
var lastExecute = _lastExecute;
55-
if (lastExecute != null)
56-
{
57-
var minExecuteInterval = _telemetryRepository._subscriptionMinExecuteInterval;
58-
var s = lastExecute.Value.Add(minExecuteInterval) - DateTime.UtcNow;
59-
if (s > TimeSpan.Zero)
60-
{
61-
Logger.LogTrace("Subscription '{Name}' minimum execute interval of {MinExecuteInterval} hit. Waiting {DelayInterval}.", Name, minExecuteInterval, s);
62-
await Task.Delay(s, cancellationToken).ConfigureAwait(false);
63-
}
64-
}
65-
66-
_lastExecute = DateTime.UtcNow;
67-
return true;
68-
}
69-
finally
70-
{
71-
_lock.Release();
72-
}
7330
}
7431

7532
public void Execute()
7633
{
77-
// Execute the subscription callback on a background thread.
78-
// The caller doesn't want to wait while the subscription is running or receive exceptions.
79-
_ = Task.Run(async () =>
80-
{
81-
// Try to queue the subscription callback.
82-
// If another caller is already in the queue then exit without calling the callback.
83-
if (!await TryQueueAsync(_cancellationToken).ConfigureAwait(false))
84-
{
85-
return;
86-
}
87-
88-
try
89-
{
90-
// Set the execution context to the one captured when the subscription was created.
91-
// This ensures that the callback runs in the same context as the subscription was created.
92-
// For example, the request culture is used to format content in the callback.
93-
//
94-
// No need to restore back to the original context because the callback is running on
95-
// a background task. The task finishes immediately after the callback.
96-
if (_executionContext != null)
97-
{
98-
ExecutionContext.Restore(_executionContext);
99-
}
100-
101-
Logger.LogTrace("Subscription '{Name}' executing.", Name);
102-
await _callback().ConfigureAwait(false);
103-
}
104-
catch (Exception ex)
105-
{
106-
Logger.LogError(ex, "Error in subscription callback");
107-
}
108-
});
34+
_callbackThrottler.Execute();
10935
}
11036

11137
public void Dispose()
11238
{
11339
_unsubscribe();
114-
_cts.Cancel();
115-
_cts.Dispose();
116-
_lock.Dispose();
40+
_callbackThrottler.Dispose();
11741
}
11842
}

0 commit comments

Comments
 (0)