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 Aspire.Dashboard.Model.Otlp;
using Microsoft.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; }
 
    internal IQueryable<TelemetryPropertyViewModel> FilteredItems =>
        _logEntryAttributes.Where(ApplyFilter).AsQueryable();
 
    internal IQueryable<TelemetryPropertyViewModel> FilteredExceptionItems =>
        _exceptionAttributes.Where(ApplyFilter).AsQueryable();
 
    internal IQueryable<TelemetryPropertyViewModel> FilteredContextItems =>
        _contextAttributes.Where(ApplyFilter).AsQueryable();
 
    internal IQueryable<TelemetryPropertyViewModel> FilteredResourceItems =>
        ViewModel.LogEntry.ApplicationView.AllProperties().Select(p => new TelemetryPropertyViewModel { Name = p.DisplayName, Key = p.Key, Value = p.Value })
            .Where(ApplyFilter).AsQueryable();
 
    private string _filter = "";
 
    private List<TelemetryPropertyViewModel> _logEntryAttributes = null!;
    private List<TelemetryPropertyViewModel> _contextAttributes = null!;
    private List<TelemetryPropertyViewModel> _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
            .Select(a => new TelemetryPropertyViewModel { Name = a.Key, Key = $"unknown-{a.Key}", Value = a.Value })
            .ToList();
 
        _contextAttributes =
        [
            new TelemetryPropertyViewModel { Name ="Category", Key = KnownStructuredLogFields.CategoryField, Value = ViewModel.LogEntry.Scope.ScopeName }
        ];
        MoveAttributes(attributes, _contextAttributes, a => a.Name is "event.name" or "logrecord.event.id" or "logrecord.event.name");
        if (HasTelemetryBaggage(ViewModel.LogEntry.TraceId))
        {
            _contextAttributes.Add(new TelemetryPropertyViewModel { Name = "TraceId", Key = KnownStructuredLogFields.TraceIdField, Value = ViewModel.LogEntry.TraceId });
        }
        if (HasTelemetryBaggage(ViewModel.LogEntry.SpanId))
        {
            _contextAttributes.Add(new TelemetryPropertyViewModel { Name = "SpanId", Key = KnownStructuredLogFields.SpanIdField, Value = ViewModel.LogEntry.SpanId });
        }
        if (HasTelemetryBaggage(ViewModel.LogEntry.ParentId))
        {
            _contextAttributes.Add(new TelemetryPropertyViewModel { Name = "ParentId", Key = KnownStructuredLogFields.ParentIdField, Value = ViewModel.LogEntry.ParentId });
        }
 
        _exceptionAttributes = [];
        MoveAttributes(attributes, _exceptionAttributes, a => a.Name.StartsWith("exception.", StringComparison.OrdinalIgnoreCase));
 
        _logEntryAttributes =
        [
            new TelemetryPropertyViewModel { Name = "Level", Key = KnownStructuredLogFields.LevelField, Value = ViewModel.LogEntry.Severity.ToString() },
            new TelemetryPropertyViewModel { Name = "Message", Key = KnownStructuredLogFields.MessageField, Value = ViewModel.LogEntry.Message },
            .. attributes,
        ];
    }
 
    private static void MoveAttributes(List<TelemetryPropertyViewModel> source, List<TelemetryPropertyViewModel> desintation, Func<TelemetryPropertyViewModel, 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(TelemetryPropertyViewModel 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;
    }
}