// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; namespace EpicGames.Core { /// /// Stores information about a span within a line /// public class LogEventSpan { /// /// Starting offset within the line /// public int Offset { get; } /// /// Text for this span /// public string Text { get; } /// /// Storage for properties /// public Dictionary Properties { get; } /// /// Constructor /// /// Starting offset within the line /// The text for this span public LogEventSpan(int offset, string text) { Offset = offset; Text = text; Properties = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// Converts this object to a string. This determines how the span will be rendered by the default console logger, so should return the original text. /// /// Original text for this span public override string ToString() { return Text; } } /// /// Individual line in the log output /// public class LogEventLine { /// /// The raw text /// public string Text { get; } /// /// List of spans for markup /// public Dictionary Spans { get; } /// /// Constructor /// /// Text for the line public LogEventLine(string text) { Text = text; Spans = []; } /// /// Adds a span containing markup on the source text /// /// Offset within the line /// Length of the span /// Name to use to identify the item in the format string /// New span for the given range public LogEventSpan AddSpan(int offset, int length, string name) { LogEventSpan span = new LogEventSpan(offset, Text.Substring(offset, length)); Spans.Add(name, span); return span; } /// /// Adds a span containing markup for a regex match group /// /// The match group /// Name to use to identify the item in the format string public LogEventSpan AddSpan(Group group, string name) { return AddSpan(group.Index, group.Length, name); } /// /// Adds a span naming a regex match group, using the name of the group /// /// The match group public LogEventSpan AddSpan(Group group) { return AddSpan(group.Index, group.Length, group.Name); } /// /// Adds a span naming a regex match group, using the name of the group /// /// The match group /// Name to use to identify the item in the format string public LogEventSpan? TryAddSpan(Group group, string name) { if (group.Success) { return AddSpan(group, name); } else { return null; } } /// /// Adds a span naming a regex match group, using the name of the group /// /// The match group public LogEventSpan? TryAddSpan(Group group) { if (group.Success) { return AddSpan(group); } else { return null; } } /// public override string ToString() { return Text; } } /// /// Allows building log events by annotating a window of lines around the current cursor position. /// public class LogEventBuilder { class LogSpan { public string _name; public int _offset; public int _length; public object? _value; public LogSpan(string name, int offset, int length, object? value) { _name = name; _offset = offset; _length = length; _value = value; } } class LogLine { public string _message; public string? _format; public Dictionary? _properties; public LogLine(string message, string? format, Dictionary? properties) { _message = message; _format = format; _properties = properties; } } /// /// The current cursor position /// public ILogCursor Current { get; private set; } /// /// The next cursor position /// public ILogCursor Next { get; private set; } /// /// Events which have been parsed so far /// List? _lines; /// /// Spans for the current line /// List? _spans; /// /// Additional properties for this line /// Dictionary? _properties; /// /// Starts building a log event at the current cursor position /// /// The current cursor position /// Number of lines to consume public LogEventBuilder(ILogCursor cursor, int lineCount = 1) { Current = cursor; Next = cursor.Rebase(1); if (lineCount > 1) { MoveNext(lineCount - 1); } } /// /// Creates a log event from the current line /// /// LogLine CreateLine() { int offset = 0; string currentLine = Current.CurrentLine!; string? format = null; if (_spans != null) { StringBuilder builder = new StringBuilder(); foreach (LogSpan span in _spans) { if (span._offset >= offset) { builder.Append(currentLine, offset, span._offset - offset); builder.Append($"{{{span._name}}}"); offset = span._offset + span._length; } } builder.Append(currentLine, offset, currentLine.Length - offset); format = builder.ToString(); } return new LogLine(currentLine, format, _properties); } /// /// Adds a span containing markup on the source text /// /// Name to use to identify the item in the format string /// Offset within the line /// Length of the span /// Data of the span /// New span for the given range public void Annotate(string name, int offset, int length, object? value = null) { LogSpan span = new LogSpan(name, offset, length, value); _properties ??= new Dictionary(StringComparer.OrdinalIgnoreCase); _properties.Add(name, span); _spans ??= []; for(int insertIdx = _spans.Count; ;insertIdx--) { if (insertIdx == 0 || _spans[insertIdx - 1]._offset < offset) { _spans.Insert(insertIdx, span); break; } } } /// /// Adds a span containing markup for a regex match group /// /// The match group /// Name to use to identify the item in the format string /// Optional value for the annotation public void Annotate(string name, Group group, object? value = null) { Annotate(name, group.Index, group.Length, value); } /// /// Adds a span naming a regex match group, using the name of the group /// /// The match group /// Optional value for the annotation public void Annotate(Group group, object? value = null) { Annotate(group.Name, group.Index, group.Length, value); } /// /// Adds a span naming a regex match group, using the name of the group /// /// The match group /// Name to use to identify the item in the format string /// Optional value for the annotation public bool TryAnnotate(string name, Group group, object? value = null) { if (group.Success) { Annotate(name, group, value); return true; } return false; } /// /// Adds a span naming a regex match group, using the name of the group /// /// The match group /// Optional value for the annotation public bool TryAnnotate(Group group, object? value = null) { if (group.Success) { Annotate(group, value); return true; } return false; } /// /// Adds an additional named property /// /// Name of the argument /// Value to associate with it public void AddProperty(string name, object value) { _properties ??= new Dictionary(StringComparer.OrdinalIgnoreCase); _properties.Add(name, value); } string? GetFirstLine() { if (_lines == null || _lines.Count == 0) { return Current.CurrentLine; } else { return _lines[0]._message; } } /// /// Check if the next line is aligned or indented from the first line /// /// True if the next line is aligned with this public bool IsNextLineAligned() => Next.IsAligned(0, GetFirstLine()); /// /// Check if the next line is indented from the first line /// /// True if the next line is aligned with this public bool IsNextLineHanging() => Next.IsHanging(0, GetFirstLine()); /// /// Complete the current line and move to the next /// public void MoveNext() { _lines ??= []; _lines.Add(CreateLine()); _spans = null; _properties = null; Current = Next; Next = Next.Rebase(1); } /// /// Advance by the given number of lines /// /// public void MoveNext(int count) { for (int idx = 0; idx < count; idx++) { MoveNext(); } } /// /// Returns an array of log events /// /// public LogEvent[] ToArray(LogLevel level, EventId eventId) { DateTime time = DateTime.UtcNow; int numLines = _lines?.Count ?? 0; int numEvents = numLines; if (Current.CurrentLine != null) { numEvents++; } LogEvent[] events = new LogEvent[numEvents]; for (int idx = 0; idx < numLines; idx++) { events[idx] = CreateEvent(time, level, eventId, idx, numEvents, _lines![idx]); } if (Current.CurrentLine != null) { events[numEvents - 1] = CreateEvent(time, level, eventId, numEvents - 1, numEvents, CreateLine()); } return events; } static LogEvent CreateEvent(DateTime time, LogLevel level, EventId eventId, int lineIndex, int lineCount, LogLine line) { Dictionary? properties = null; if (line._properties != null) { properties = []; foreach ((string name, object value) in line._properties) { object newValue; if (value is LogSpan span) { string text = line._message.Substring(span._offset, span._length); if (span._value == null) { newValue = text; } else if (span._value is LogValue newLogValue) { newLogValue.Text = text; newValue = newLogValue; } else { newValue = text; } } else { newValue = value; } properties[name] = newValue; } } return new LogEvent(time, level, eventId, lineIndex, lineCount, line._message, line._format, properties, null); } /// /// Creates a match object at the given priority /// /// /// The event id /// /// public LogEventMatch ToMatch(LogEventPriority priority, LogLevel level, EventId eventId) { return new LogEventMatch(priority, ToArray(level, eventId)); } } }