| File: src\Shared\ConsoleLogs\LogEntry.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// 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.Shared.ConsoleLogs; [DebuggerDisplay("LineNumber = {LineNumber}, Timestamp = {Timestamp}, ResourcePrefix = {ResourcePrefix}, Content = {Content}, Type = {Type}")] #if ASPIRE_DASHBOARD public sealed class LogEntry #else internal sealed class LogEntry #endif { public string? Content { get; private set; } /// <summary> /// The text content of the log entry. This is the same as <see cref="Content"/>, but without embedded links or other transformations and including the timestamp. /// </summary> public string? RawContent { get; private set; } private string? _strippedRawContent; private string? _strippedLogContent; /// <summary> /// <see cref="RawContent"/> with ANSI control sequences removed. This is the plain text the user /// actually sees rendered (including the timestamp). Use this for matching against user-entered /// text - <see cref="Content"/> contains HTML markup added during ANSI conversion and /// <see cref="RawContent"/> still contains the raw escape sequences, so matching against either /// produces false negatives when the term spans markup or a color boundary (for example, the /// default .NET console emits the level prefix as <c>info\x1b[..m:</c>, so a search for /// <c>info:</c> would never match the raw content). /// </summary> /// <remarks> /// The stripped value is cached because filtering runs over the entire log buffer on each update. /// </remarks> public string? GetStrippedRawContent() { if (RawContent is null) { return null; } return _strippedRawContent ??= AnsiParser.StripControlSequences(RawContent); } /// <summary> /// <see cref="RawContent"/> with the parsed timestamp and ANSI control sequences removed. This /// represents the rendered log message text without optional row adornments such as timestamps, /// resource prefixes, or stderr badges. /// </summary> /// <remarks> /// The stripped value is cached because filtering runs over the entire log buffer on each update. /// </remarks> public string? GetStrippedLogContent() { if (RawContent is null) { return null; } _strippedLogContent ??= TimestampParser.TryParseConsoleTimestamp(RawContent, out var timestampParseResult) ? AnsiParser.StripControlSequences(timestampParseResult.Value.ModifiedText) : GetStrippedRawContent(); return _strippedLogContent; } public DateTime? Timestamp { get; private set; } public LogEntryType Type { get; private set; } = LogEntryType.Default; public int LineNumber { get; set; } public LogPauseViewModel? Pause { get; private set; } public string? ResourcePrefix { get; set; } public static LogEntry CreatePause(string resourcePrefix, DateTime startTimestamp, DateTime? endTimestamp = null) { return new LogEntry { Timestamp = startTimestamp, Type = LogEntryType.Pause, LineNumber = 0, Pause = new LogPauseViewModel { ResourcePrefix = resourcePrefix, StartTime = startTimestamp, EndTime = endTimestamp }, ResourcePrefix = resourcePrefix }; } public static LogEntry Create(DateTime? timestamp, string logMessage, bool isErrorMessage) { return Create(timestamp, logMessage, logMessage, isErrorMessage, resourcePrefix: null); } public static LogEntry Create(DateTime? timestamp, string logMessage, string rawLogContent, bool isErrorMessage, string? resourcePrefix) { return new LogEntry { Timestamp = timestamp, Content = logMessage, RawContent = rawLogContent, ResourcePrefix = resourcePrefix, Type = isErrorMessage ? LogEntryType.Error : LogEntryType.Default }; } } /// <summary> /// Represents the stable identity fields used to deduplicate overlapping log entries. /// </summary> /// <remarks> /// Keep this key aligned with the fields that come from the underlying log source. Display-only /// fields such as line number and resource prefix can differ depending on whether the entry came /// from a terminal snapshot, a follow stream, or an in-memory logger replay, so they are excluded. /// </remarks> internal readonly record struct LogEntryKey(DateTime? Timestamp, string? Content, string? RawContent, LogEntryType Type) { /// <summary> /// Creates a deduplication key for the specified log entry. /// </summary> public static LogEntryKey Create(LogEntry logEntry) { return new(logEntry.Timestamp, logEntry.Content, logEntry.RawContent, logEntry.Type); } } #if ASPIRE_DASHBOARD public enum LogEntryType #else internal enum LogEntryType #endif { Default, Error, Pause }