1- using System . Collections . Concurrent ;
1+ using System . Collections . Concurrent ;
22using System . Globalization ;
33using System . Reflection ;
4+ using System . Security . Cryptography ;
5+ using System . Text ;
6+ using System . Text . RegularExpressions ;
47using DebugProbe . AspNetCore . Internal . Utils ;
58using DebugProbe . AspNetCore . Models ;
69using DebugProbe . AspNetCore . Options ;
@@ -12,6 +15,19 @@ namespace DebugProbe.AspNetCore.Storage;
1215/// </summary>
1316public class DebugEntryStore
1417{
18+ private static readonly Regex GuidRegex = new ( @"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b" , RegexOptions . Compiled ) ;
19+ private static readonly Regex NumberRegex = new ( @"\b\d+(\.\d+)?\b" , RegexOptions . Compiled ) ;
20+
21+ /// <summary>
22+ /// Gets the static instance of DebugEntryStore.
23+ /// </summary>
24+ public static DebugEntryStore ? Instance { get ; private set ; }
25+
26+ /// <summary>
27+ /// Gets the exception groups.
28+ /// </summary>
29+ public ConcurrentDictionary < string , ExceptionGroup > ExceptionGroups { get ; } = new ( ) ;
30+
1531 /// <summary>
1632 /// Gets environment information for the current application.
1733 /// </summary>
@@ -22,6 +38,7 @@ public class DebugEntryStore
2238
2339 public DebugEntryStore ( DebugProbeOptions options )
2440 {
41+ Instance = this ;
2542 _limit = options . MaxEntries ;
2643
2744 Environment = new DebugEnvironment
@@ -41,8 +58,33 @@ public void Add(DebugEntry entry)
4158 {
4259 _queue . Enqueue ( entry ) ;
4360
61+ if ( TryParseException ( entry . ResponseBody , out var type , out var message ) )
62+ {
63+ var normalizedMessage = NormalizeMessage ( message ) ;
64+ var fingerprint = ComputeHash ( type + normalizedMessage ) ;
65+
66+ ExceptionGroups . AddOrUpdate ( fingerprint ,
67+ key => new ExceptionGroup
68+ {
69+ Fingerprint = fingerprint ,
70+ Type = type ,
71+ SampleMessage = message ,
72+ Count = 1 ,
73+ LastSeen = DateTimeOffset . UtcNow
74+ } ,
75+ ( key , existing ) => new ExceptionGroup
76+ {
77+ Fingerprint = existing . Fingerprint ,
78+ Type = existing . Type ,
79+ SampleMessage = existing . SampleMessage ,
80+ Count = existing . Count + 1 ,
81+ LastSeen = DateTimeOffset . UtcNow
82+ } ) ;
83+ }
84+
4485 while ( _queue . Count > _limit )
4586 {
87+ // ExceptionGroups counts are a running tally and must NOT be decremented on MaxEntries eviction.
4688 _queue . TryDequeue ( out _ ) ;
4789 }
4890 }
@@ -60,6 +102,63 @@ public List<DebugEntry> GetAll()
60102 public void Clear ( )
61103 {
62104 while ( _queue . TryDequeue ( out _ ) ) { }
105+ ExceptionGroups . Clear ( ) ;
106+ }
107+
108+ private static bool TryParseException ( string ? body , out string type , out string message )
109+ {
110+ type = string . Empty ;
111+ message = string . Empty ;
112+
113+ if ( string . IsNullOrWhiteSpace ( body ) )
114+ {
115+ return false ;
116+ }
117+
118+ var firstLine = body . Split ( new [ ] { '\r ' , '\n ' } , StringSplitOptions . RemoveEmptyEntries ) . FirstOrDefault ( ) ;
119+ if ( string . IsNullOrWhiteSpace ( firstLine ) )
120+ {
121+ return false ;
122+ }
123+
124+ var colonIndex = firstLine . IndexOf ( ':' ) ;
125+ if ( colonIndex <= 0 )
126+ {
127+ var trimmed = firstLine . Trim ( ) ;
128+ if ( ! trimmed . Contains ( ' ' ) && trimmed . EndsWith ( "Exception" ) )
129+ {
130+ type = trimmed ;
131+ message = string . Empty ;
132+ return true ;
133+ }
134+ return false ;
135+ }
136+
137+ var potentialType = firstLine [ ..colonIndex ] . Trim ( ) ;
138+ if ( potentialType . Contains ( ' ' ) || ! potentialType . EndsWith ( "Exception" ) )
139+ {
140+ return false ;
141+ }
142+
143+ type = potentialType ;
144+ message = firstLine [ ( colonIndex + 1 ) ..] . Trim ( ) ;
145+ return true ;
146+ }
147+
148+ private static string NormalizeMessage ( string message )
149+ {
150+ if ( string . IsNullOrEmpty ( message ) )
151+ return string . Empty ;
152+
153+ var normalized = GuidRegex . Replace ( message , "*" ) ;
154+ normalized = NumberRegex . Replace ( normalized , "*" ) ;
155+ return normalized ;
156+ }
157+
158+ private static string ComputeHash ( string input )
159+ {
160+ var bytes = SHA256 . HashData ( Encoding . UTF8 . GetBytes ( input ) ) ;
161+ return Convert . ToHexString ( bytes ) ;
63162 }
64163
65164 private static string GetDateFormat ( )
0 commit comments