// 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));
}
}
}