Skip to content

Commit 878d8d6

Browse files
committed
Adds Model - Store
1 parent f20213b commit 878d8d6

17 files changed

Lines changed: 514 additions & 16 deletions

File tree

ai-exception-insights-starter/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@
2424
<groupId>org.springframework.ai</groupId>
2525
<artifactId>spring-ai-starter-model-openai</artifactId>
2626
</dependency>
27+
28+
<dependency>
29+
<groupId>org.springframework.boot</groupId>
30+
<artifactId>spring-boot-starter-webmvc</artifactId>
31+
<optional>true</optional>
32+
</dependency>
33+
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter-test</artifactId>
37+
<scope>test</scope>
38+
</dependency>
2739
</dependencies>
2840

2941
</project>

ai-exception-insights-starter/src/main/java/io/github/rexrk/autoconfigure/AiExceptionInsightsAutoConfiguration.java

Lines changed: 0 additions & 4 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.github.rexrk.exception.insights.autoconfigure;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
import java.time.Duration;
6+
7+
@ConfigurationProperties(AiExceptionInsightProperties.prefix)
8+
public class AiExceptionInsightProperties {
9+
public static final String prefix = "devtools.ai.exception-insights";
10+
11+
private boolean enabled = true;
12+
private int maxEvents = 10;
13+
private Duration deduplicationWindow = Duration.ofSeconds(5);
14+
private int logBufferSize = 5;
15+
16+
// getters and setters
17+
public boolean isEnabled() { return enabled; }
18+
public void setEnabled(boolean enabled) { this.enabled = enabled; }
19+
public int getMaxEvents() { return maxEvents; }
20+
public void setMaxEvents(int maxEvents) { this.maxEvents = maxEvents; }
21+
public int getLogBufferSize() { return logBufferSize; }
22+
public void setLogBufferSize(int logBufferSize) { this.logBufferSize = logBufferSize; }
23+
public Duration getDeduplicationWindow() { return deduplicationWindow; }
24+
public void setDeduplicationWindow(Duration d) { this.deduplicationWindow = d; }
25+
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.github.rexrk.exception.insights.autoconfigure;
2+
3+
import io.github.rexrk.exception.insights.store.InMemoryErrorEventStore;
4+
import org.springframework.boot.autoconfigure.AutoConfiguration;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
7+
import org.springframework.context.annotation.Bean;
8+
9+
@AutoConfiguration
10+
@ConditionalOnProperty(
11+
prefix = AiExceptionInsightProperties.prefix,
12+
name = "enabled",
13+
matchIfMissing = true
14+
)
15+
@EnableConfigurationProperties(AiExceptionInsightProperties.class)
16+
public class AiExceptionInsightsAutoConfiguration {
17+
18+
@Bean
19+
public InMemoryErrorEventStore errorEventStore(
20+
AiExceptionInsightProperties props
21+
) {
22+
return new InMemoryErrorEventStore(
23+
props.getMaxEvents(),
24+
props.getDeduplicationWindow()
25+
);
26+
}
27+
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.github.rexrk.exception.insights.model;
2+
3+
import java.util.List;
4+
5+
public record AiExplanation(
6+
String summary,
7+
List<String> causes,
8+
List<String> fixes,
9+
String rawResponse
10+
) {}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.github.rexrk.exception.insights.model;
2+
3+
import java.util.List;
4+
5+
public record AiPromptContext(
6+
7+
// One-line summary
8+
String exceptionClass,
9+
String exceptionMessage,
10+
11+
// Trimmed stack (top frames only)
12+
List<String> topStackFrames,
13+
14+
// Last few meaningful logs (already formatted)
15+
List<String> recentLogMessages,
16+
17+
// What kind of execution this was
18+
String executionContext // e.g. "HTTP POST /users", "Scheduled task: cleanupJob"
19+
) {}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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("\tat ").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("\tat ").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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.rexrk.exception.insights.model;
2+
3+
import java.time.Instant;
4+
5+
public record LogLine(
6+
String level,
7+
String message,
8+
String loggerName,
9+
String threadName,
10+
String throwableClass,
11+
String throwableMessage,
12+
Instant timestamp
13+
) {}

0 commit comments

Comments
 (0)