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