1+ package io .github .rexrk .exception .insights .model ;
2+
3+ import java .time .Instant ;
4+ import java .util .Collections ;
5+ import java .util .HashMap ;
6+ import java .util .List ;
7+ import java .util .Map ;
8+ import java .util .UUID ;
9+
10+ public class ErrorEvent {
11+
12+ public enum Type {
13+ HTTP_REQUEST ,
14+ ASYNC ,
15+ SCHEDULED ,
16+ TRANSACTIONAL ,
17+ EVENT_LISTENER ,
18+ UNCAUGHT_THREAD
19+ }
20+
21+ // --- Identity ---
22+ private final String id ;
23+ private final Instant timestamp ;
24+ private final Type type ;
25+
26+ // --- Exception details ---
27+ private final String exceptionClass ;
28+ private final String message ;
29+ private final String stackTrace ;
30+ private final String rootCauseClass ;
31+ private final String rootCauseMessage ;
32+ private final String fingerprint ;
33+
34+ // --- HTTP context (only populated for HTTP_REQUEST type) ---
35+ private final String httpMethod ;
36+ private final String requestUri ;
37+ private final Map <String , String > requestHeaders ;
38+ private final String requestBody ;
39+
40+ // --- General context (thread name, method name, etc.) ---
41+ private final Map <String , String > context ;
42+
43+ // --- Log lines captured just before this error occurred ---
44+ private final List <LogLine > recentLogs ;
45+
46+ // --- Mutable: set asynchronously after AI call completes ---
47+ private volatile AiExplanation aiExplanation ;
48+
49+ private ErrorEvent (Builder builder ) {
50+ this .id = UUID .randomUUID ().toString ();
51+ this .timestamp = builder .timestamp != null ? builder .timestamp : Instant .now ();
52+ this .type = builder .type ;
53+
54+ this .exceptionClass = builder .exceptionClass ;
55+ this .message = builder .message ;
56+ this .stackTrace = builder .stackTrace ;
57+ this .rootCauseClass = builder .rootCauseClass ;
58+ this .rootCauseMessage = builder .rootCauseMessage ;
59+ this .fingerprint = builder .fingerprint ;
60+
61+ this .httpMethod = builder .httpMethod ;
62+ this .requestUri = builder .requestUri ;
63+ this .requestHeaders = builder .requestHeaders .isEmpty ()
64+ ? Collections .emptyMap ()
65+ : Map .copyOf (builder .requestHeaders );
66+ this .requestBody = builder .requestBody ;
67+
68+ this .context = builder .context .isEmpty ()
69+ ? Collections .emptyMap ()
70+ : Map .copyOf (builder .context );
71+ this .recentLogs = builder .recentLogs .isEmpty ()
72+ ? Collections .emptyList ()
73+ : Collections .unmodifiableList (builder .recentLogs );
74+
75+ this .aiExplanation = null ;
76+ }
77+
78+ // The only setter — called by AiExplanationService when the async call returns
79+ public void setAiExplanation (AiExplanation aiExplanation ) {
80+ this .aiExplanation = aiExplanation ;
81+ }
82+
83+ public static Builder builder () {
84+ return new Builder ();
85+ }
86+
87+ // -------------------------------------------------------------------------
88+
89+ public static final class Builder {
90+
91+ private Instant timestamp ;
92+ private Type type ;
93+
94+ private String exceptionClass ;
95+ private String message ;
96+ private String stackTrace ;
97+ private String rootCauseClass ;
98+ private String rootCauseMessage ;
99+ private String fingerprint ;
100+
101+ private String httpMethod ;
102+ private String requestUri ;
103+ private final Map <String , String > requestHeaders = new HashMap <>();
104+ private String requestBody ;
105+
106+ private final Map <String , String > context = new HashMap <>();
107+ private List <LogLine > recentLogs = Collections .emptyList ();
108+
109+ private Builder () {}
110+
111+ public Builder type (Type type ) {
112+ this .type = type ;
113+ return this ;
114+ }
115+
116+ public Builder timestamp (Instant timestamp ) {
117+ this .timestamp = timestamp ;
118+ return this ;
119+ }
120+
121+ /**
122+ * Extracts everything from the throwable in one call:
123+ * class, message, root cause, stack trace string, and fingerprint.
124+ */
125+ public Builder exception (Throwable ex ) {
126+ this .exceptionClass = ex .getClass ().getName ();
127+ this .message = ex .getMessage ();
128+ this .stackTrace = buildStackTraceString (ex );
129+ this .fingerprint = buildFingerprint (ex );
130+
131+ Throwable root = ex ;
132+ while (root .getCause () != null ) {
133+ root = root .getCause ();
134+ }
135+ this .rootCauseClass = root .getClass ().getName ();
136+ this .rootCauseMessage = root .getMessage ();
137+
138+ return this ;
139+ }
140+
141+ public Builder httpMethod (String httpMethod ) {
142+ this .httpMethod = httpMethod ;
143+ return this ;
144+ }
145+
146+ public Builder requestUri (String requestUri ) {
147+ this .requestUri = requestUri ;
148+ return this ;
149+ }
150+
151+ public Builder requestHeaders (Map <String , String > headers ) {
152+ this .requestHeaders .putAll (headers );
153+ return this ;
154+ }
155+
156+ public Builder requestBody (String requestBody ) {
157+ this .requestBody = requestBody ;
158+ return this ;
159+ }
160+
161+ public Builder context (String key , String value ) {
162+ this .context .put (key , value );
163+ return this ;
164+ }
165+
166+ public Builder recentLogs (List <LogLine > recentLogs ) {
167+ this .recentLogs = recentLogs ;
168+ return this ;
169+ }
170+
171+ public ErrorEvent build () {
172+ if (type == null ) {
173+ throw new IllegalStateException ("ErrorEvent.Type is required" );
174+ }
175+ if (exceptionClass == null ) {
176+ throw new IllegalStateException ("Call .exception(Throwable) before .build()" );
177+ }
178+ return new ErrorEvent (this );
179+ }
180+
181+ // --- Private helpers ---
182+
183+ private String buildStackTraceString (Throwable ex ) {
184+ StringBuilder sb = new StringBuilder ();
185+ sb .append (ex ).append ("\n " );
186+ for (StackTraceElement frame : ex .getStackTrace ()) {
187+ sb .append ("\t at " ).append (frame ).append ("\n " );
188+ }
189+ Throwable cause = ex .getCause ();
190+ if (cause != null ) {
191+ sb .append ("Caused by: " ).append (cause ).append ("\n " );
192+ for (StackTraceElement frame : cause .getStackTrace ()) {
193+ sb .append ("\t at " ).append (frame ).append ("\n " );
194+ }
195+ }
196+ return sb .toString ();
197+ }
198+
199+ private String buildFingerprint (Throwable ex ) {
200+ StringBuilder sb = new StringBuilder (ex .getClass ().getName ());
201+ StackTraceElement [] frames = ex .getStackTrace ();
202+ int limit = Math .min (3 , frames .length );
203+ for (int i = 0 ; i < limit ; i ++) {
204+ sb .append ("|" )
205+ .append (frames [i ].getClassName ())
206+ .append ("." )
207+ .append (frames [i ].getMethodName ())
208+ .append (":" )
209+ .append (frames [i ].getLineNumber ());
210+ }
211+ // hex string, short and stable
212+ return Integer .toHexString (sb .toString ().hashCode ());
213+ }
214+ }
215+
216+ // -------------------------------------------------------------------------
217+ // Getters
218+ // -------------------------------------------------------------------------
219+
220+ public String getId () { return id ; }
221+ public Instant getTimestamp () { return timestamp ; }
222+ public Type getType () { return type ; }
223+ public String getExceptionClass () { return exceptionClass ; }
224+ public String getMessage () { return message ; }
225+ public String getStackTrace () { return stackTrace ; }
226+ public String getRootCauseClass () { return rootCauseClass ; }
227+ public String getRootCauseMessage () { return rootCauseMessage ; }
228+ public String getFingerprint () { return fingerprint ; }
229+ public String getHttpMethod () { return httpMethod ; }
230+ public String getRequestUri () { return requestUri ; }
231+ public Map <String , String > getRequestHeaders () { return requestHeaders ; }
232+ public String getRequestBody () { return requestBody ; }
233+ public Map <String , String > getContext () { return context ; }
234+ public List <LogLine > getRecentLogs () { return recentLogs ; }
235+ public AiExplanation getAiExplanation () { return aiExplanation ; }
236+ }
0 commit comments