Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Horde/Issues/IssueEvent.cs
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

312 lines
8.3 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
namespace EpicGames.Horde.Issues
{
/// <summary>
/// Wraps a log event and allows it to be tagged by issue handlers
/// </summary>
public class IssueEvent
{
/// <summary>
/// Index of the line within this log
/// </summary>
public int LineIndex { get; }
/// <summary>
/// Severity of the event
/// </summary>
public LogLevel Severity { get; }
/// <summary>
/// The type of event
/// </summary>
public EventId? EventId { get; }
/// <summary>
/// Gets this event data as a BSON document
/// </summary>
public IReadOnlyList<JsonLogEvent> Lines { get; }
/// <summary>
/// <see cref="IssueAuditLogger"/> attached to this event
/// </summary>
public IssueAuditLogger? AuditLogger { get; set; }
/// <summary>
/// Constructor
/// </summary>
public IssueEvent(int lineIndex, LogLevel severity, EventId? eventId, IReadOnlyList<JsonLogEvent> lines)
{
LineIndex = lineIndex;
Severity = severity;
EventId = eventId;
Lines = lines;
}
/// <summary>
/// Renders the entire message of this event
/// </summary>
public string Render()
=> String.Join("\n", Lines.Select(x => x.GetRenderedMessage().ToString()));
/// <inheritdoc/>
public override string ToString() => $"[{LineIndex}] {Render()}";
}
/// <summary>
/// A group of <see cref="IssueEvent"/> objects with their fingerprint
/// </summary>
public class IssueEventGroup
{
/// <summary>
/// The type of issue, which defines the handler to use for it
/// </summary>
public string Type { get; set; }
/// <summary>
/// Template string for the issue summary
/// </summary>
public string SummaryTemplate { get; set; }
/// <summary>
/// List of keys which identify this issue.
/// </summary>
public HashSet<IssueKey> Keys { get; } = new HashSet<IssueKey>();
/// <summary>
/// Collection of additional metadata added by the handler
/// </summary>
public HashSet<IssueMetadata> Metadata { get; } = new HashSet<IssueMetadata>();
/// <summary>
/// Filter for changes that should be included in this issue
/// </summary>
public string ChangeFilter { get; set; }
/// <summary>
/// Individual log events
/// </summary>
public List<IssueEvent> Events { get; } = new List<IssueEvent>();
/// <summary>
/// Constructor
/// </summary>
/// <param name="type">The type of issue</param>
/// <param name="summaryTemplate">Template for the summary string to display for the issue</param>
/// <param name="changeFilter">Filter for changes covered by this issue</param>
public IssueEventGroup(string type, string summaryTemplate, string changeFilter)
{
Type = type;
SummaryTemplate = summaryTemplate;
ChangeFilter = changeFilter;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="type">The type of issue</param>
/// <param name="summaryTemplate">Template for the summary string to display for the issue</param>
/// <param name="changeFilter">Filter for changes covered by this issue</param>
public IssueEventGroup(string type, string summaryTemplate, IReadOnlyList<string> changeFilter)
: this(type, summaryTemplate, String.Join(";", changeFilter))
{
}
}
/// <summary>
/// Temporary log buffer to store information about an <see cref="IssueEvent"/> before it's assigned to a build health issue
/// </summary>
public class IssueAuditLogger : ILogger
{
/// <inheritdoc/>
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel) => true;
private readonly List<IssueAuditLogEntry> _entries;
/// <summary>
/// Constructor
/// </summary>
public IssueAuditLogger()
{
_entries = new List<IssueAuditLogEntry>();
}
/// <summary>
/// Gets list of <see cref="IssueAuditLogEntry"/> objects in the buffer
/// </summary>
/// <returns>List of log entries</returns>
public List<IssueAuditLogEntry> GetEntries()
{
return _entries;
}
/// <summary>
/// Stores a <see cref="IssueAuditLogEntry"/> in the log buffer
/// </summary>
/// <param name="logLevel">Entry will be written on this level.</param>
/// <param name="eventId">Id of the event.</param>
/// <param name="state">The entry to be written. Can be also an object.</param>
/// <param name="exception">The exception related to this entry.</param>
/// <param name="formatter">Function to create a <see cref="String"/> message of the <paramref name="state"/> and <paramref name="exception"/>.</param>
/// <typeparam name="TState">The type of the object to be written.</typeparam>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
// extract and store message template and args
string? messageTemplate = null;
List<object> args = [];
IEnumerable<KeyValuePair<string, object>>? innerEnumerable = state as IEnumerable<KeyValuePair<string, object>>;
if (innerEnumerable != null)
{
foreach (KeyValuePair<string, object> pair in innerEnumerable)
{
if (pair.Key.Equals("{OriginalFormat}", StringComparison.Ordinal))
{
messageTemplate = pair.Value.ToString();
}
else
{
args.Add(pair.Value);
}
}
}
IssueAuditLogEntry entry = new IssueAuditLogEntry(logLevel, messageTemplate ?? "", [.. args]);
_entries.Add(entry);
}
}
/// <summary>
/// Buffered log entry stored in a <see cref="IssueAuditLogger"/>
/// </summary>
/// <remarks>
/// Constructor
/// </remarks>
public readonly struct IssueAuditLogEntry(LogLevel logLevel, string messageTemplate, object[]? args) : IEquatable<IssueAuditLogEntry>
{
/// <summary>
/// Severity level at which to log the entry
/// </summary>
private readonly LogLevel _logLevel = logLevel;
/// <summary>
/// Message template for structured log entry
/// </summary>
private readonly string _messageTemplate = messageTemplate;
/// <summary>
/// Arguments to be injected in the message template of the entry
/// </summary>
private readonly object[]? _args = args;
/// <summary>
/// Log level accessor
/// </summary>
public readonly LogLevel GetLogLevel()
{
return _logLevel;
}
/// <summary>
/// Message template accessor
/// </summary>
public readonly string? GetMessageTemplate()
{
return _messageTemplate;
}
/// <summary>
/// Args accessor
/// </summary>
public readonly object[]? GetArgs()
{
return _args;
}
/// <summary>
/// Compares against another IssueAuditLogEntry object for equality
/// </summary>
/// <param name="otherEntry">Other entry to compare against</param>
/// <returns>True if the two entries have the same log level, message template, and arguments.</returns>
public readonly bool Equals(IssueAuditLogEntry otherEntry)
{
if (_logLevel != otherEntry._logLevel || _messageTemplate != otherEntry._messageTemplate)
{
return false;
}
if (_args == null && otherEntry._args == null)
{
return true;
}
else if (_args == null || otherEntry._args == null)
{
return false;
}
else
{
if (_args.Length != otherEntry._args.Length)
{
return false;
}
for (int i = 0; i < _args.Length; i++)
{
if (_args[i] == null && otherEntry._args[i] == null)
{
continue;
}
else if (_args[i] == null || otherEntry._args[i] == null)
{
return false;
}
else if (!_args[i].Equals(otherEntry._args[i]))
{
return false;
}
}
}
return true;
}
/// <summary>
/// Returns the underlying value hashcode
/// </summary>
public override readonly int GetHashCode()
{
return HashCode.Combine(_logLevel, _messageTemplate);
}
/// <inheritdoc/>
public override readonly bool Equals(object? obj)
{
return obj is IssueAuditLogEntry entry && Equals(entry);
}
/// <summary>
/// Compares two IssueAuditLogEntry objects for equality
/// </summary>
public static bool operator ==(IssueAuditLogEntry left, IssueAuditLogEntry right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two IssueAuditLogEntry objects for inequality
/// </summary>
public static bool operator !=(IssueAuditLogEntry left, IssueAuditLogEntry right)
{
return !(left == right);
}
}
}