File: Components\Controls\StructuredLogDetails.razor.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Model;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
 
namespace Aspire.Dashboard.Components.Controls;
 
public partial class StructuredLogDetails
{
    [Parameter, EditorRequired]
    public required StructureLogsDetailsViewModel ViewModel { get; set; }
 
    [Inject]
    public required BrowserTimeProvider TimeProvider { get; init; }
 
    private IQueryable<LogEntryPropertyViewModel> FilteredItems =>
        _logEntryAttributes.Select(p => new LogEntryPropertyViewModel { Name = p.Key, Value = p.Value })
            .Where(ApplyFilter).AsQueryable();
 
    private IQueryable<LogEntryPropertyViewModel> FilteredExceptionItems =>
        _exceptionAttributes.Select(p => new LogEntryPropertyViewModel { Name = p.Key, Value = p.Value })
            .Where(ApplyFilter).AsQueryable();
 
    private IQueryable<LogEntryPropertyViewModel> FilteredContextItems =>
        _contextAttributes.Select(p => new LogEntryPropertyViewModel { Name = p.Key, Value = p.Value })
            .Where(ApplyFilter).AsQueryable();
 
    private IQueryable<LogEntryPropertyViewModel> FilteredResourceItems =>
        ViewModel.LogEntry.Application.AllProperties().Select(p => new LogEntryPropertyViewModel { Name = p.Key, Value = p.Value })
            .Where(ApplyFilter).AsQueryable();
 
    private string _filter = "";
 
    private readonly GridSort<LogEntryPropertyViewModel> _nameSort = GridSort<LogEntryPropertyViewModel>.ByAscending(vm => vm.Name);
    private readonly GridSort<LogEntryPropertyViewModel> _valueSort = GridSort<LogEntryPropertyViewModel>.ByAscending(vm => vm.Value);
    private List<KeyValuePair<string, string>> _logEntryAttributes = null!;
    private List<KeyValuePair<string, string>> _contextAttributes = null!;
    private List<KeyValuePair<string, string>> _exceptionAttributes = null!;
 
    protected override void OnParametersSet()
    {
        // Move some attributes to separate lists, e.g. exception attributes to their own list.
        // Remaining attributes are displayed along side the message.
        var attributes = ViewModel.LogEntry.Attributes.ToList();
 
        _contextAttributes =
        [
            new KeyValuePair<string, string>("Category", ViewModel.LogEntry.Scope.ScopeName)
        ];
        MoveAttributes(attributes, _contextAttributes, a => a.Key is "event.name" or "logrecord.event.id" or "logrecord.event.name");
        if (HasTelemetryBaggage(ViewModel.LogEntry.TraceId))
        {
            _contextAttributes.Add(new KeyValuePair<string, string>("TraceId", ViewModel.LogEntry.TraceId));
        }
        if (HasTelemetryBaggage(ViewModel.LogEntry.SpanId))
        {
            _contextAttributes.Add(new KeyValuePair<string, string>("SpanId", ViewModel.LogEntry.SpanId));
        }
        if (HasTelemetryBaggage(ViewModel.LogEntry.ParentId))
        {
            _contextAttributes.Add(new KeyValuePair<string, string>("ParentId", ViewModel.LogEntry.ParentId));
        }
 
        _exceptionAttributes = [];
        MoveAttributes(attributes, _exceptionAttributes, a => a.Key.StartsWith("exception.", StringComparison.OrdinalIgnoreCase));
 
        _logEntryAttributes =
        [
            new KeyValuePair<string, string>("Level", ViewModel.LogEntry.Severity.ToString()),
            new KeyValuePair<string, string>("Message", ViewModel.LogEntry.Message),
            .. attributes,
        ];
    }
 
    private static void MoveAttributes(List<KeyValuePair<string, string>> source, List<KeyValuePair<string, string>> desintation, Func<KeyValuePair<string, string>, bool> predicate)
    {
        var insertStart = desintation.Count;
        for (var i = source.Count - 1; i >= 0; i--)
        {
            if (predicate(source[i]))
            {
                desintation.Insert(insertStart, source[i]);
                source.RemoveAt(i);
            }
        }
    }
 
    private bool ApplyFilter(LogEntryPropertyViewModel vm)
    {
        return vm.Name.Contains(_filter, StringComparison.CurrentCultureIgnoreCase) ||
            vm.Value?.Contains(_filter, StringComparison.CurrentCultureIgnoreCase) == true;
    }
 
    // Sometimes a parent ID is added and the value is 0000000000. Don't display unhelpful IDs.
    private static bool HasTelemetryBaggage(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return false;
        }
 
        for (var i = 0; i < value.Length; i++)
        {
            if (value[i] != '0')
            {
                return true;
            }
        }
 
        return false;
    }
}