diff --git a/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs b/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs index 68fe698ab87..1388f25990c 100644 --- a/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs +++ b/src/Aspire.Dashboard/Components/Dialogs/SettingsDialog.razor.cs @@ -39,10 +39,9 @@ public partial class SettingsDialog : IDialogContentComponent, IDisposable protected override void OnInitialized() { - // Order cultures in the dropdown with invariant culture. This prevents the order of languages changing when the culture changes. - _languageOptions = [.. GlobalizationHelpers.LocalizedCultures.OrderBy(c => c.NativeName, StringComparer.InvariantCultureIgnoreCase)]; + _languageOptions = GlobalizationHelpers.OrderedLocalizedCultures; - _selectedUiCulture = GlobalizationHelpers.TryGetKnownParentCulture(_languageOptions, CultureInfo.CurrentUICulture, out var matchedCulture) + _selectedUiCulture = GlobalizationHelpers.TryGetKnownParentCulture(CultureInfo.CurrentUICulture, out var matchedCulture) ? matchedCulture : // Otherwise, Blazor has fallen back to a supported language CultureInfo.CurrentUICulture; diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs index e6c08390814..6c3a1ed6d4f 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs @@ -11,33 +11,6 @@ public partial class LogMessageColumnDisplay protected override void OnInitialized() { - _exceptionText = GetExceptionText(); - } - - private string? GetExceptionText() - { - // exception.stacktrace includes the exception message and type. - // https://opentelemetry.io/docs/specs/semconv/attributes-registry/exception/ - if (GetProperty("exception.stacktrace") is { Length: > 0 } stackTrace) - { - return stackTrace; - } - - if (GetProperty("exception.message") is { Length: > 0 } message) - { - if (GetProperty("exception.type") is { Length: > 0 } type) - { - return $"{type}: {message}"; - } - - return message; - } - - return null; - - string? GetProperty(string propertyName) - { - return LogEntry.Attributes.GetValue(propertyName); - } + _exceptionText = OtlpLogEntry.GetExceptionText(LogEntry); } } diff --git a/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs b/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs index ecff1e52a0a..f310189cddb 100644 --- a/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs +++ b/src/Aspire.Dashboard/DashboardEndpointsBuilder.cs @@ -52,7 +52,7 @@ await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.Si } // The passed in language should be one of the localized cultures. - var newLanguage = GlobalizationHelpers.LocalizedCultures.SingleOrDefault(c => string.Equals(c.Name, language, StringComparisons.CultureName)); + var newLanguage = GlobalizationHelpers.OrderedLocalizedCultures.SingleOrDefault(c => string.Equals(c.Name, language, StringComparisons.CultureName)); if (newLanguage == null) { return Results.BadRequest(); diff --git a/src/Aspire.Dashboard/Extensions/ResourceViewModelExtensions.cs b/src/Aspire.Dashboard/Extensions/ResourceViewModelExtensions.cs index 256c5717062..86c7a470269 100644 --- a/src/Aspire.Dashboard/Extensions/ResourceViewModelExtensions.cs +++ b/src/Aspire.Dashboard/Extensions/ResourceViewModelExtensions.cs @@ -42,6 +42,11 @@ public static bool IsNotStarted(this ResourceViewModel resource) return resource.KnownState is KnownResourceState.NotStarted; } + public static bool IsWaiting(this ResourceViewModel resource) + { + return resource.KnownState is KnownResourceState.Waiting; + } + public static bool IsUnknownState(this ResourceViewModel resource) => resource.KnownState is KnownResourceState.Unknown; public static bool HasNoState(this ResourceViewModel resource) => string.IsNullOrEmpty(resource.State); diff --git a/src/Aspire.Dashboard/Model/DebugSessionHelpers.cs b/src/Aspire.Dashboard/Model/DebugSessionHelpers.cs index 8ec77e53966..c90cf13773a 100644 --- a/src/Aspire.Dashboard/Model/DebugSessionHelpers.cs +++ b/src/Aspire.Dashboard/Model/DebugSessionHelpers.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -10,7 +10,7 @@ namespace Aspire.Dashboard.Model; internal static class DebugSessionHelpers { - public static HttpClient CreateHttpClient(Uri debugSessionUri, string token, X509Certificate2? cert, Func? createHandler) + public static HttpClient CreateHttpClient(Uri? debugSessionUri, string? token, X509Certificate2? cert, Func? createHandler) { var handler = new HttpClientHandler(); if (cert is not null) @@ -23,23 +23,34 @@ public static HttpClient CreateHttpClient(Uri debugSessionUri, string token, X50 return true; } + if (c == null) + { + return false; + } + // Certificate isn't immediately valid. Check if it is the same as the one we expect. // It's ok that comparison isn't time constant because this is public information. - return cert.RawData.SequenceEqual(c?.RawData); + return cert.RawData.SequenceEqual(c.RawData); }; } var resolvedHandler = createHandler?.Invoke(handler) ?? handler; var client = new HttpClient(resolvedHandler) { - BaseAddress = debugSessionUri, - DefaultRequestHeaders = - { - { "Authorization", $"Bearer {token}" }, - { "User-Agent", "Aspire Dashboard" } - } + Timeout = Timeout.InfiniteTimeSpan }; + if (debugSessionUri is not null) + { + client.BaseAddress = debugSessionUri; + } + + client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Aspire Dashboard"); + if (token != null) + { + client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}"); + } + return client; } diff --git a/src/Aspire.Dashboard/Model/ResourceStateViewModel.cs b/src/Aspire.Dashboard/Model/ResourceStateViewModel.cs index 7882248f983..df851ecd371 100644 --- a/src/Aspire.Dashboard/Model/ResourceStateViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceStateViewModel.cs @@ -126,6 +126,14 @@ internal static string GetResourceStateTooltip(ResourceViewModel resource, IStri // DCP reports the container runtime is unhealthy. Most likely the container runtime (e.g. Docker) isn't running. return loc[nameof(Columns.StateColumnResourceContainerRuntimeUnhealthy)]; } + else if (resource.IsWaiting()) + { + return loc[nameof(Columns.StateColumnResourceWaiting)]; + } + else if (resource.IsNotStarted()) + { + return loc[nameof(Columns.StateColumnResourceNotStarted)]; + } // Fallback to text displayed in column. return GetStateText(resource, loc); diff --git a/src/Aspire.Dashboard/Model/ResourceViewModel.cs b/src/Aspire.Dashboard/Model/ResourceViewModel.cs index 5423b22931c..ff68b380714 100644 --- a/src/Aspire.Dashboard/Model/ResourceViewModel.cs +++ b/src/Aspire.Dashboard/Model/ResourceViewModel.cs @@ -103,9 +103,14 @@ public bool IsResourceHidden() } public static string GetResourceName(ResourceViewModel resource, IDictionary allResources) + { + return GetResourceName(resource, allResources.Values); + } + + public static string GetResourceName(ResourceViewModel resource, IEnumerable allResources) { var count = 0; - foreach (var (_, item) in allResources) + foreach (var item in allResources) { if (item.IsResourceHidden()) { diff --git a/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs b/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs index cb57d96d7d5..77b22886867 100644 --- a/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs +++ b/src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs @@ -24,6 +24,8 @@ public static class OtlpHelpers WriteIndented = false }; + public const int ShortenedIdLength = 7; + public static ApplicationKey GetApplicationKey(this Resource resource) { string? serviceName = null; @@ -62,7 +64,7 @@ public static ApplicationKey GetApplicationKey(this Resource resource) return new ApplicationKey(serviceName, serviceInstanceId ?? serviceName); } - public static string ToShortenedId(string id) => TruncateString(id, maxLength: 7); + public static string ToShortenedId(string id) => TruncateString(id, maxLength: ShortenedIdLength); public static string ToHexString(ReadOnlyMemory bytes) { @@ -429,6 +431,20 @@ public static PagedResult GetItems(IEnumerable= ShortenedIdLength) + { + return existingId.StartsWith(incomingId, StringComparison.OrdinalIgnoreCase); + } + else + { + return existingId.Equals(incomingId, StringComparison.OrdinalIgnoreCase); + } + } + public static bool TryAddScope(Dictionary scopes, InstrumentationScope? scope, OtlpContext context, [NotNullWhen(true)] out OtlpScope? s) { try diff --git a/src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs b/src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs index 09f370c775e..50e32b6bd0e 100644 --- a/src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs +++ b/src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs @@ -7,9 +7,11 @@ namespace Aspire.Dashboard.Otlp.Model; -[DebuggerDisplay("TimeStamp = {TimeStamp}, Severity = {Severity}, Message = {Message}")] +[DebuggerDisplay("InternalId = {InternalId}, TimeStamp = {TimeStamp}, Severity = {Severity}, Message = {Message}")] public class OtlpLogEntry { + private static long s_nextLogEntryId; + public KeyValuePair[] Attributes { get; } public DateTime TimeStamp { get; } public uint Flags { get; } @@ -21,11 +23,11 @@ public class OtlpLogEntry public string? OriginalFormat { get; } public OtlpApplicationView ApplicationView { get; } public OtlpScope Scope { get; } - public Guid InternalId { get; } + public long InternalId { get; } public OtlpLogEntry(LogRecord record, OtlpApplicationView logApp, OtlpScope scope, OtlpContext context) { - InternalId = Guid.NewGuid(); + InternalId = Interlocked.Increment(ref s_nextLogEntryId); TimeStamp = ResolveTimeStamp(record); string? originalFormat = null; @@ -118,4 +120,35 @@ private static DateTime ResolveTimeStamp(LogRecord record) _ => log.Attributes.GetValue(field) }; } + + public const string ExceptionStackTraceField = "exception.stacktrace"; + public const string ExceptionMessageField = "exception.message"; + public const string ExceptionTypeField = "exception.type"; + + public static string? GetExceptionText(OtlpLogEntry logEntry) + { + // exception.stacktrace includes the exception message and type. + // https://opentelemetry.io/docs/specs/semconv/attributes-registry/exception/ + if (GetProperty(logEntry, ExceptionStackTraceField) is { Length: > 0 } stackTrace) + { + return stackTrace; + } + + if (GetProperty(logEntry, ExceptionMessageField) is { Length: > 0 } message) + { + if (GetProperty(logEntry, ExceptionTypeField) is { Length: > 0 } type) + { + return $"{type}: {message}"; + } + + return message; + } + + return null; + + static string? GetProperty(OtlpLogEntry logEntry, string propertyName) + { + return logEntry.Attributes.GetValue(propertyName); + } + } } diff --git a/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs b/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs index 5b3b4206b3c..9772d8bba96 100644 --- a/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs +++ b/src/Aspire.Dashboard/Otlp/Storage/Subscription.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Aspire.Dashboard.Utils; namespace Aspire.Dashboard.Otlp.Storage; @@ -10,18 +11,10 @@ public sealed class Subscription : IDisposable { private static int s_subscriptionId; - private readonly Func _callback; - private readonly ExecutionContext? _executionContext; - private readonly TelemetryRepository _telemetryRepository; - private readonly CancellationTokenSource _cts; - private readonly CancellationToken _cancellationToken; + private readonly CallbackThrottler _callbackThrottler; private readonly Action _unsubscribe; - private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - private ILogger Logger => _telemetryRepository._otlpContext.Logger; private readonly int _subscriptionId = Interlocked.Increment(ref s_subscriptionId); - private DateTime? _lastExecute; - public int SubscriptionId => _subscriptionId; public ApplicationKey? ApplicationKey { get; } public SubscriptionType SubscriptionType { get; } @@ -32,87 +25,18 @@ public Subscription(string name, ApplicationKey? applicationKey, SubscriptionTyp Name = name; ApplicationKey = applicationKey; SubscriptionType = subscriptionType; - _callback = callback; + _callbackThrottler = new CallbackThrottler(name, telemetryRepository._otlpContext.Logger, telemetryRepository._subscriptionMinExecuteInterval, callback, executionContext); _unsubscribe = unsubscribe; - _executionContext = executionContext; - _telemetryRepository = telemetryRepository; - _cts = new CancellationTokenSource(); - _cancellationToken = _cts.Token; - } - - private async Task TryQueueAsync(CancellationToken cancellationToken) - { - var success = _lock.Wait(0, cancellationToken); - if (!success) - { - Logger.LogDebug("Subscription '{Name}' update already queued.", Name); - return false; - } - - try - { - var lastExecute = _lastExecute; - if (lastExecute != null) - { - var minExecuteInterval = _telemetryRepository._subscriptionMinExecuteInterval; - var s = lastExecute.Value.Add(minExecuteInterval) - DateTime.UtcNow; - if (s > TimeSpan.Zero) - { - Logger.LogTrace("Subscription '{Name}' minimum execute interval of {MinExecuteInterval} hit. Waiting {DelayInterval}.", Name, minExecuteInterval, s); - await Task.Delay(s, cancellationToken).ConfigureAwait(false); - } - } - - _lastExecute = DateTime.UtcNow; - return true; - } - finally - { - _lock.Release(); - } } public void Execute() { - // Execute the subscription callback on a background thread. - // The caller doesn't want to wait while the subscription is running or receive exceptions. - _ = Task.Run(async () => - { - // Try to queue the subscription callback. - // If another caller is already in the queue then exit without calling the callback. - if (!await TryQueueAsync(_cancellationToken).ConfigureAwait(false)) - { - return; - } - - try - { - // Set the execution context to the one captured when the subscription was created. - // This ensures that the callback runs in the same context as the subscription was created. - // For example, the request culture is used to format content in the callback. - // - // No need to restore back to the original context because the callback is running on - // a background task. The task finishes immediately after the callback. - if (_executionContext != null) - { - ExecutionContext.Restore(_executionContext); - } - - Logger.LogTrace("Subscription '{Name}' executing.", Name); - await _callback().ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in subscription callback"); - } - }); + _callbackThrottler.Execute(); } public void Dispose() { _unsubscribe(); - _cts.Cancel(); - _cts.Dispose(); - _lock.Dispose(); + _callbackThrottler.Dispose(); } } diff --git a/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs b/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs index 9b18ab5ff7d..f22533e5a31 100644 --- a/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs +++ b/src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs @@ -778,16 +778,15 @@ public Dictionary GetLogsFieldValues(string attributeName) { Debug.Assert(_tracesLock.IsReadLockHeld || _tracesLock.IsWriteLockHeld, $"Must get lock before calling {nameof(GetTraceUnsynchronized)}."); - try - { - var results = _traces.Where(t => t.TraceId.StartsWith(traceId, StringComparison.Ordinal)); - var trace = results.SingleOrDefault(); - return trace is not null ? OtlpTrace.Clone(trace) : null; - } - catch (Exception ex) + foreach (var trace in _traces) { - throw new InvalidOperationException($"Multiple traces found with trace id '{traceId}'.", ex); + if (OtlpHelpers.MatchTelemetryId(traceId, trace.TraceId)) + { + return OtlpTrace.Clone(trace); + } } + + return null; } private OtlpSpan? GetSpanUnsynchronized(string traceId, string spanId) diff --git a/src/Aspire.Dashboard/Resources/Columns.Designer.cs b/src/Aspire.Dashboard/Resources/Columns.Designer.cs index 0cbbe1b9f21..e2ac677e9cf 100644 --- a/src/Aspire.Dashboard/Resources/Columns.Designer.cs +++ b/src/Aspire.Dashboard/Resources/Columns.Designer.cs @@ -97,7 +97,7 @@ public static string ResourceNameDisplayProcessIdText { } /// - /// Looks up a localized string similar to Resource is running but not in a healthy state. Click for details.. + /// Looks up a localized string similar to Resource is running but not in a healthy state.. /// public static string RunningAndUnhealthyResourceStateToolTip { get { @@ -214,6 +214,24 @@ public static string StateColumnResourceExitedUnexpectedly { } } + /// + /// Looks up a localized string similar to Resource has not started because it's configured to not automatically start.. + /// + public static string StateColumnResourceNotStarted { + get { + return ResourceManager.GetString("StateColumnResourceNotStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resource is waiting for other resources to be in a running and healthy state.. + /// + public static string StateColumnResourceWaiting { + get { + return ResourceManager.GetString("StateColumnResourceWaiting", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unknown. /// diff --git a/src/Aspire.Dashboard/Resources/Columns.resx b/src/Aspire.Dashboard/Resources/Columns.resx index 6a293e74dcf..27075b983c3 100644 --- a/src/Aspire.Dashboard/Resources/Columns.resx +++ b/src/Aspire.Dashboard/Resources/Columns.resx @@ -184,7 +184,7 @@ URLs - Resource is running but not in a healthy state. Click for details. + Resource is running but not in a healthy state. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -195,4 +195,10 @@ For more information, see https://aka.ms/dotnet/aspire/container-runtime-unhealthy Contains a new line + + Resource has not started because it's configured to not automatically start. + + + Resource is waiting for other resources to be in a running and healthy state. + \ No newline at end of file diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf index ac604b9c009..21c1b6101f8 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - Prostředek je spuštěný, ale není v pořádku. Kliknutím zobrazíte podrobnosti. + Resource is running but not in a healthy state. + Prostředek je spuštěný, ale není v pořádku. Kliknutím zobrazíte podrobnosti. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Další informace najdete na https://aka.ms/dotnet/aspire/container-runtime-unhe Prostředek {0} byl neočekávaně ukončen s ukončovacím kódem {1}. {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Neznámé diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf index 75c7ba84320..69bdc1169f4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - Die Ressource wird ausgeführt, befindet sich jedoch nicht in einem fehlerfreien Zustand. Klicken Sie hier, um Einzelheiten anzuzeigen. + Resource is running but not in a healthy state. + Die Ressource wird ausgeführt, befindet sich jedoch nicht in einem fehlerfreien Zustand. Klicken Sie hier, um Einzelheiten anzuzeigen. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Weitere Informationen finden Sie unter https://aka.ms/dotnet/aspire/container-ru {0} wurde unerwartet mit dem Exitcode {1} beendet. {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Unbekannt diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf index 59c916c6487..dcc79e8100d 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - El recurso se está ejecutando, pero no en un estado correcto. Haga clic aquí para obtener detalles. + Resource is running but not in a healthy state. + El recurso se está ejecutando, pero no en un estado correcto. Haga clic aquí para obtener detalles. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Para obtener más información, consulte https://aka.ms/dotnet/aspire/container- {0} se cerró inesperadamente con el código de salida {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Desconocido diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf index b209f79745d..ea7cf0355b4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - La ressource est en cours d’exécution, mais son état n’est pas sain. Cliquez pour plus de détails. + Resource is running but not in a healthy state. + La ressource est en cours d’exécution, mais son état n’est pas sain. Cliquez pour plus de détails. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Pour plus d’informations, consultez https://aka.ms/dotnet/aspire/container-run {0} s’est arrêté de manière inattendue avec le code {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Inconnu diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf index b30893f7bc4..752529e14bb 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - La risorsa è in esecuzione ma non è in uno stato integro. Fare clic per i dettagli. + Resource is running but not in a healthy state. + La risorsa è in esecuzione ma non è in uno stato integro. Fare clic per i dettagli. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Per altre informazioni, vedere https://aka.ms/dotnet/aspire/container-runtime-un La risorsa {0} è stata chiusa in modo imprevisto con codice di uscita {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Sconosciuto diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf index 60f19f8b749..9a68851e373 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - リソースは実行中ですが、正常な状態ではありません。クリックして詳細を表示してください。 + Resource is running but not in a healthy state. + リソースは実行中ですが、正常な状態ではありません。クリックして詳細を表示してください。 Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ For more information, see https://aka.ms/dotnet/aspire/container-runtime-unhealt {0} は終了コード{1} で予期せず終了しました {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown 不明 diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf index 2ad8042995a..ea821fa14c5 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - 리소스가 실행 중이지만 정상 상태가 아닙니다. 자세한 내용을 보려면 클릭하세요. + Resource is running but not in a healthy state. + 리소스가 실행 중이지만 정상 상태가 아닙니다. 자세한 내용을 보려면 클릭하세요. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ For more information, see https://aka.ms/dotnet/aspire/container-runtime-unhealt {0}(이)가 종료 코드 {1}(으)로 예기치 않게 종료되었습니다. {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown 알 수 없음 diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf index 7f9104a7f89..46da8fe1908 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - Zasób jest uruchomiony, ale nie jest w dobrej kondycji. Kliknij, aby uzyskać szczegółowe informacje. + Resource is running but not in a healthy state. + Zasób jest uruchomiony, ale nie jest w dobrej kondycji. Kliknij, aby uzyskać szczegółowe informacje. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Aby uzyskać więcej informacji, zobacz https://aka.ms/dotnet/aspire/container-r Nieoczekiwanie zakończony {0} z kodem zakończenia {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Nieznane diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf index bb7e45b02fe..99912e8eaf6 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - O recurso está em execução, mas não está em um estado íntegro. Clique para obter detalhes. + Resource is running but not in a healthy state. + O recurso está em execução, mas não está em um estado íntegro. Clique para obter detalhes. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Para obter mais informações, consulte https://aka.ms/dotnet/aspire/container- {0} saiu inesperadamente com o código de saída {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Desconhecido diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf index 0eda4ecb784..ea9387e5e0f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - Ресурс работает, но находится в неработоспособном состоянии. Щелкните, чтобы получить дополнительные сведения. + Resource is running but not in a healthy state. + Ресурс работает, но находится в неработоспособном состоянии. Щелкните, чтобы получить дополнительные сведения. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ For more information, see https://aka.ms/dotnet/aspire/container-runtime-unhealt {0} неожиданно завершила работу, вернув код {1}. {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Неизвестно diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf index bce0d7ab16b..3701efdde8f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - Kaynak çalışıyor ancak iyi durumda değil. Ayrıntılar için tıklayın. + Resource is running but not in a healthy state. + Kaynak çalışıyor ancak iyi durumda değil. Ayrıntılar için tıklayın. Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ Daha fazla bilgi için bkz. https://aka.ms/dotnet/aspire/container-runtime-unhea {0} {1} çıkış kodu ile beklenmedik bir şekilde çıktı {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown Bilinmiyor diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf index 0d3b9a00fa6..ac60b126277 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - 资源正在运行,但状态不正常。单击以显示详细信息。 + Resource is running but not in a healthy state. + 资源正在运行,但状态不正常。单击以显示详细信息。 Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ For more information, see https://aka.ms/dotnet/aspire/container-runtime-unhealt {0} 已意外退出,退出代码为 {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown 未知 diff --git a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf index 0b8143da57a..2dd779278ca 100644 --- a/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf @@ -23,8 +23,8 @@ {0} is a numeric id - Resource is running but not in a healthy state. Click for details. - 資源正在執行,但未處於良好的狀態。按一下以查看詳細資料。 + Resource is running but not in a healthy state. + 資源正在執行,但未處於良好的狀態。按一下以查看詳細資料。 Tool tip text explaining that the resource is running but not yet ready to receive requests. @@ -89,6 +89,16 @@ For more information, see https://aka.ms/dotnet/aspire/container-runtime-unhealt {0} 意外結束,結束代碼: {1} {0} is a resource type, {1} is a number + + Resource has not started because it's configured to not automatically start. + Resource has not started because it's configured to not automatically start. + + + + Resource is waiting for other resources to be in a running and healthy state. + Resource is waiting for other resources to be in a running and healthy state. + + Unknown 未知 diff --git a/src/Aspire.Dashboard/Utils/CallbackThrottler.cs b/src/Aspire.Dashboard/Utils/CallbackThrottler.cs new file mode 100644 index 00000000000..5a484f1e781 --- /dev/null +++ b/src/Aspire.Dashboard/Utils/CallbackThrottler.cs @@ -0,0 +1,110 @@ +// 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; + +namespace Aspire.Dashboard.Utils; + +[DebuggerDisplay("Name = {Name}")] +public sealed class CallbackThrottler +{ + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private DateTime? _lastExecute; + + public CallbackThrottler(string name, ILogger logger, TimeSpan minExecuteInterval, Func callback, ExecutionContext? executionContext) + { + Name = name; + _logger = logger; + _minExecuteInterval = minExecuteInterval; + _callback = callback; + _executionContext = executionContext; + _cts = new CancellationTokenSource(); + _cancellationToken = _cts.Token; + } + + public string Name { get; } + + private readonly ILogger _logger; + private readonly TimeSpan _minExecuteInterval; + private readonly Func _callback; + private readonly ExecutionContext? _executionContext; + private readonly CancellationTokenSource _cts; + private readonly CancellationToken _cancellationToken; + + private async Task TryQueueAsync(CancellationToken cancellationToken) + { + var success = _lock.Wait(0, cancellationToken); + if (!success) + { + _logger.LogTrace("Callback '{Name}' update already queued.", Name); + return false; + } + + try + { + var lastExecute = _lastExecute; + if (lastExecute != null) + { + var minExecuteInterval = _minExecuteInterval; + var s = lastExecute.Value.Add(minExecuteInterval) - DateTime.UtcNow; + if (s > TimeSpan.Zero) + { + _logger.LogTrace("Callback '{Name}' minimum execute interval of {MinExecuteInterval} hit. Waiting {DelayInterval}.", Name, minExecuteInterval, s); + await Task.Delay(s, cancellationToken).ConfigureAwait(false); + } + } + + _lastExecute = DateTime.UtcNow; + return true; + } + finally + { + _lock.Release(); + } + } + + private async Task ExecuteAsync() + { + // Try to queue the subscription callback. + // If another caller is already in the queue then exit without calling the callback. + if (!await TryQueueAsync(_cancellationToken).ConfigureAwait(false)) + { + return; + } + + try + { + // Set the execution context to the one captured when the subscription was created. + // This ensures that the callback runs in the same context as the subscription was created. + // For example, the request culture is used to format content in the callback. + // + // No need to restore back to the original context because the callback is running on + // a background task. The task finishes immediately after the callback. + if (_executionContext != null) + { + ExecutionContext.Restore(_executionContext); + } + + _logger.LogTrace("Callback '{Name}' executing.", Name); + await _callback().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in callback."); + } + } + + public void Execute() + { + // Execute on a background thread. + // The caller doesn't want to wait while the execution is running or receive exceptions. + _ = Task.Run(ExecuteAsync); + } + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + _lock.Dispose(); + } +} diff --git a/src/Aspire.Dashboard/Utils/GlobalizationHelpers.cs b/src/Aspire.Dashboard/Utils/GlobalizationHelpers.cs index ea516d4e6f7..6d19f2e611a 100644 --- a/src/Aspire.Dashboard/Utils/GlobalizationHelpers.cs +++ b/src/Aspire.Dashboard/Utils/GlobalizationHelpers.cs @@ -13,7 +13,7 @@ internal static class GlobalizationHelpers { private const int MaxCultureParentDepth = 5; - public static List LocalizedCultures { get; } + public static List OrderedLocalizedCultures { get; } public static List AllCultures { get; } @@ -27,11 +27,14 @@ static GlobalizationHelpers() "en", "cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant", // Standard cultures for compliance. }; - LocalizedCultures = localizedCultureNames.Select(CultureInfo.GetCultureInfo).ToList(); + var localizedCultureInfos = localizedCultureNames.Select(CultureInfo.GetCultureInfo).ToList(); AllCultures = GetAllCultures(); - ExpandedLocalizedCultures = GetExpandedLocalizedCultures(LocalizedCultures, AllCultures); + ExpandedLocalizedCultures = GetExpandedLocalizedCultures(localizedCultureInfos, AllCultures); + + // Order cultures for display in the UI with invariant culture. This prevents the order of languages changing when the culture changes. + OrderedLocalizedCultures = localizedCultureInfos.OrderBy(c => c.NativeName, StringComparer.InvariantCultureIgnoreCase).ToList(); } private static Dictionary> GetExpandedLocalizedCultures(List localizedCultures, List allCultures) @@ -87,6 +90,11 @@ private static List GetAllCultures() return allCultures; } + public static bool TryGetKnownParentCulture(CultureInfo culture, [NotNullWhen(true)] out CultureInfo? matchedCulture) + { + return TryGetKnownParentCulture(OrderedLocalizedCultures, culture, out matchedCulture); + } + public static bool TryGetKnownParentCulture(List knownCultures, CultureInfo culture, [NotNullWhen(true)] out CultureInfo? matchedCulture) { if (knownCultures.Contains(culture)) diff --git a/tests/Aspire.Dashboard.Tests/GlobalizationHelpersTests.cs b/tests/Aspire.Dashboard.Tests/GlobalizationHelpersTests.cs index dd4ae6da626..58816078b86 100644 --- a/tests/Aspire.Dashboard.Tests/GlobalizationHelpersTests.cs +++ b/tests/Aspire.Dashboard.Tests/GlobalizationHelpersTests.cs @@ -19,7 +19,7 @@ public void ExpandedLocalizedCultures_IncludesPopularCultures() .ToList(); // Assert - foreach (var localizedCulture in GlobalizationHelpers.LocalizedCultures) + foreach (var localizedCulture in GlobalizationHelpers.OrderedLocalizedCultures) { Assert.Contains(localizedCulture.Name, supportedCultures); }