// Copyright Epic Games, Inc. All Rights Reserved. using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace EpicGames.Horde.Issues.Handlers { /// /// Instance of a particular compile error /// [IssueHandler] public class HashedIssueHandler : IssueHandler { readonly IssueHandlerContext _context; readonly List _issueEvents = new List(); /// /// Known general events /// static readonly HashSet s_knownGeneralEvents = new HashSet { KnownLogEvents.Generic, KnownLogEvents.ExitCode, KnownLogEvents.Horde, KnownLogEvents.Horde_InvalidPreflight }; /// /// Constructor /// public HashedIssueHandler(IssueHandlerContext context) => _context = context; /// /// Determines if the given event is general and should be salted to make it unique /// /// The event id to compare /// True if the given event id matches public static bool IsGeneralEventId(EventId eventId) { return s_knownGeneralEvents.Contains(eventId) || (eventId.Id >= KnownLogEvents.Systemic.Id && eventId.Id <= KnownLogEvents.Systemic_Max.Id); } /// public override int Priority => 1; /// public override bool HandleEvent(IssueEvent logEvent) { _issueEvents.Add(logEvent); return true; } /// public override IEnumerable GetIssues() { List issues = new List(); IssueEventGroup? genericFingerprint = null; IssueEventGroup? genericErrorsFingerprint = null; HashSet hashes = new HashSet(); // keep hash consistent when only have general, non-unique events bool allGeneral = _issueEvents.FirstOrDefault(stepEvent => stepEvent.EventId == null || !IsGeneralEventId(stepEvent.EventId.Value)) == null; foreach (IssueEvent stepEvent in _issueEvents) { string hashSource = stepEvent.Render(); if (!allGeneral && stepEvent.EventId != null) { // If the event is general, salt the hash with the stream id, template, otherwise it will be aggressively matched. // Consider salting with node name, though template id should be enough and have better grouping if (IsGeneralEventId(stepEvent.EventId.Value)) { hashSource += $"step:{_context.StreamId}:{_context.TemplateId}"; } } if (hashes.Count < 25 && TryGetHash(hashSource, out Md5Hash hash)) { hashes.Add(hash); IssueEventGroup issue = new IssueEventGroup("Hashed", "{Severity} in {Meta:Node}", IssueChangeFilter.All); issue.Events.Add(stepEvent); issue.Keys.AddHash(hash); issue.Metadata.Add("Node", _context.NodeName); issues.Add(issue); } else { if (stepEvent.Severity == LogLevel.Error || stepEvent.Severity == LogLevel.Critical) { if (genericErrorsFingerprint == null) { genericErrorsFingerprint = new IssueEventGroup("Hashed", "{Severity} in {Meta:Node}", IssueChangeFilter.All); genericErrorsFingerprint.Keys.Add(IssueKey.FromStepAndSeverity(_context.StreamId, _context.TemplateId, _context.NodeName, LogLevel.Error)); genericErrorsFingerprint.Metadata.Add("Node", _context.NodeName); issues.Add(genericErrorsFingerprint); } genericErrorsFingerprint.Events.Add(stepEvent); } else { if (genericFingerprint == null) { genericFingerprint = new IssueEventGroup("Hashed", "{Severity} in {Meta:Node}", IssueChangeFilter.All); genericFingerprint.Keys.Add(IssueKey.FromStep(_context.StreamId, _context.TemplateId, _context.NodeName)); genericFingerprint.Metadata.Add("Node", _context.NodeName); issues.Add(genericFingerprint); } genericFingerprint.Events.Add(stepEvent); } } } return issues; } static bool TryGetHash(string message, out Md5Hash hash) { string sanitized = message.ToUpperInvariant(); sanitized = Regex.Replace(sanitized, @"(? 30) { hash = Md5Hash.Compute(Encoding.UTF8.GetBytes(sanitized)); return true; } else { hash = Md5Hash.Zero; return false; } } } }