Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/DashboardEndpointsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 20 additions & 9 deletions src/Aspire.Dashboard/Model/DebugSessionHelpers.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,7 +10,7 @@ namespace Aspire.Dashboard.Model;

internal static class DebugSessionHelpers
{
public static HttpClient CreateHttpClient(Uri debugSessionUri, string token, X509Certificate2? cert, Func<HttpClientHandler, HttpMessageHandler>? createHandler)
public static HttpClient CreateHttpClient(Uri? debugSessionUri, string? token, X509Certificate2? cert, Func<HttpClientHandler, HttpMessageHandler>? createHandler)
{
var handler = new HttpClientHandler();
if (cert is not null)
Expand All @@ -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;
}

Expand Down
8 changes: 8 additions & 0 deletions src/Aspire.Dashboard/Model/ResourceStateViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,14 @@ public bool IsResourceHidden()
}

public static string GetResourceName(ResourceViewModel resource, IDictionary<string, ResourceViewModel> allResources)
{
return GetResourceName(resource, allResources.Values);
}

public static string GetResourceName(ResourceViewModel resource, IEnumerable<ResourceViewModel> allResources)
{
var count = 0;
foreach (var (_, item) in allResources)
foreach (var item in allResources)
{
if (item.IsResourceHidden())
{
Expand Down
18 changes: 17 additions & 1 deletion src/Aspire.Dashboard/Otlp/Model/OtlpHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<byte> bytes)
{
Expand Down Expand Up @@ -429,6 +431,20 @@ public static PagedResult<TResult> GetItems<TSource, TResult>(IEnumerable<TSourc
};
}

public static bool MatchTelemetryId(string incomingId, string existingId)
{
// This method uses StartsWith to find a match.
// We only want to use that logic if the traceId is at least the length of a shortened id.
if (incomingId.Length >= ShortenedIdLength)
{
return existingId.StartsWith(incomingId, StringComparison.OrdinalIgnoreCase);
}
else
{
return existingId.Equals(incomingId, StringComparison.OrdinalIgnoreCase);
}
}

public static bool TryAddScope(Dictionary<string, OtlpScope> scopes, InstrumentationScope? scope, OtlpContext context, [NotNullWhen(true)] out OtlpScope? s)
{
try
Expand Down
39 changes: 36 additions & 3 deletions src/Aspire.Dashboard/Otlp/Model/OtlpLogEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>[] Attributes { get; }
public DateTime TimeStamp { get; }
public uint Flags { get; }
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why tho? More efficient

TimeStamp = ResolveTimeStamp(record);

string? originalFormat = null;
Expand Down Expand Up @@ -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);
}
}
}
86 changes: 5 additions & 81 deletions src/Aspire.Dashboard/Otlp/Storage/Subscription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -10,18 +11,10 @@ public sealed class Subscription : IDisposable
{
private static int s_subscriptionId;

private readonly Func<Task> _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; }
Expand All @@ -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<bool> 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();
}
}
Loading
Loading