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