Skip to content

Commit eece907

Browse files
authored
Merge pull request #4 from podmortem/start-ai-interface
Start the analysis endpoint for ai-interface
2 parents f290b46 + 8900a94 commit eece907

File tree

9 files changed

+360
-43
lines changed

9 files changed

+360
-43
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ jobs:
1313
name: Test Build on Pull Request
1414
if: github.event_name == 'pull_request'
1515
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
packages: read
1619
steps:
1720
- name: Checkout repository
1821
uses: actions/checkout@v4

pom.xml

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616
<skipITs>true</skipITs>
1717
<surefire-plugin.version>3.5.3</surefire-plugin.version>
1818

19-
<podmortem.common.lib.version>1.0-f3f9123-SNAPSHOT</podmortem.common.lib.version>
19+
<podmortem.common.lib.version>1.0-35533c7-SNAPSHOT</podmortem.common.lib.version>
20+
<podmortem.ai.provider.lib.version>1.0-073da55-SNAPSHOT</podmortem.ai.provider.lib.version>
2021
</properties>
2122

2223
<repositories>
2324
<repository>
24-
<id>github</id>
25-
<name>GitHub podmortem Apache Maven Packages</name>
25+
<id>github-common</id>
26+
<name>Common Library</name>
2627
<url>https://maven.pkg.github.com/podmortem/common-lib</url>
2728
</repository>
29+
<repository>
30+
<id>github-ai-provider</id>
31+
<name>AI Provider Library</name>
32+
<url>https://maven.pkg.github.com/podmortem/ai-provider-lib</url>
33+
</repository>
2834
</repositories>
2935

3036
<dependencyManagement>
@@ -48,11 +54,33 @@
4854
<groupId>io.quarkus</groupId>
4955
<artifactId>quarkus-arc</artifactId>
5056
</dependency>
57+
<dependency>
58+
<groupId>io.quarkus</groupId>
59+
<artifactId>quarkus-mutiny</artifactId>
60+
</dependency>
61+
<dependency>
62+
<groupId>io.quarkus</groupId>
63+
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
64+
</dependency>
65+
<dependency>
66+
<groupId>io.quarkus</groupId>
67+
<artifactId>quarkus-cache</artifactId>
68+
</dependency>
69+
<dependency>
70+
<groupId>io.quarkus</groupId>
71+
<artifactId>quarkus-rest-client-jackson</artifactId>
72+
</dependency>
5173
<dependency>
5274
<groupId>com.redhat.podmortem</groupId>
5375
<artifactId>common</artifactId>
5476
<version>${podmortem.common.lib.version}</version>
5577
</dependency>
78+
<dependency>
79+
<groupId>com.redhat.podmortem</groupId>
80+
<artifactId>provider</artifactId>
81+
<version>${podmortem.ai.provider.lib.version}</version>
82+
</dependency>
83+
5684
<dependency>
5785
<groupId>io.quarkus</groupId>
5886
<artifactId>quarkus-junit5</artifactId>

src/main/java/com/redhat/podmortem/GreetingResource.java

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.redhat.podmortem.ai.rest;
2+
3+
import com.redhat.podmortem.ai.service.AnalysisService;
4+
import com.redhat.podmortem.common.model.analysis.AnalysisRequest;
5+
import io.smallrye.mutiny.Uni;
6+
import jakarta.enterprise.context.ApplicationScoped;
7+
import jakarta.inject.Inject;
8+
import jakarta.ws.rs.*;
9+
import jakarta.ws.rs.core.MediaType;
10+
import jakarta.ws.rs.core.Response;
11+
import java.util.Map;
12+
import org.jboss.logging.Logger;
13+
14+
@Path("/api/v1/ai-analysis")
15+
@ApplicationScoped
16+
public class Analysis {
17+
18+
private static final Logger LOG = Logger.getLogger(Analysis.class);
19+
20+
@Inject AnalysisService aiAnalysisService;
21+
22+
@POST
23+
@Path("/explain")
24+
@Produces(MediaType.APPLICATION_JSON)
25+
@Consumes(MediaType.APPLICATION_JSON)
26+
public Uni<Response> explainFailure(AnalysisRequest request) {
27+
LOG.infof(
28+
"Received AI analysis request for analysis ID: %s",
29+
request.getAnalysisResult().getAnalysisId());
30+
31+
return aiAnalysisService
32+
.analyzeFailure(request.getAnalysisResult(), request.getProviderConfig())
33+
.map(
34+
response -> {
35+
LOG.infof(
36+
"AI analysis completed for analysis ID: %s",
37+
request.getAnalysisResult().getAnalysisId());
38+
return Response.ok(response).build();
39+
})
40+
.onFailure()
41+
.recoverWithItem(
42+
throwable -> {
43+
LOG.errorf(
44+
throwable,
45+
"AI analysis failed for analysis ID: %s",
46+
request.getAnalysisResult().getAnalysisId());
47+
return Response.status(500)
48+
.entity(
49+
Map.of(
50+
"error", "AI analysis failed",
51+
"message", throwable.getMessage(),
52+
"analysisId",
53+
request.getAnalysisResult()
54+
.getAnalysisId()))
55+
.build();
56+
});
57+
}
58+
59+
@GET
60+
@Path("/health")
61+
@Produces(MediaType.APPLICATION_JSON)
62+
public Response healthCheck() {
63+
return Response.ok(
64+
Map.of(
65+
"status", "UP",
66+
"service", "ai-interface",
67+
"timestamp", System.currentTimeMillis()))
68+
.build();
69+
}
70+
71+
@GET
72+
@Path("/providers")
73+
@Produces(MediaType.APPLICATION_JSON)
74+
public Uni<Response> listProviders() {
75+
return aiAnalysisService
76+
.getAvailableProviders()
77+
.map(providers -> Response.ok(Map.of("providers", providers)).build())
78+
.onFailure()
79+
.recoverWithItem(
80+
throwable ->
81+
Response.status(500)
82+
.entity(
83+
Map.of(
84+
"error",
85+
"Failed to list providers",
86+
"message",
87+
throwable.getMessage()))
88+
.build());
89+
}
90+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.redhat.podmortem.ai.service;
2+
3+
import com.redhat.podmortem.common.model.analysis.AnalysisResult;
4+
import com.redhat.podmortem.common.model.provider.*;
5+
import io.smallrye.mutiny.Uni;
6+
import jakarta.enterprise.context.ApplicationScoped;
7+
import jakarta.inject.Inject;
8+
import java.time.Duration;
9+
import java.time.Instant;
10+
import java.time.temporal.ChronoUnit;
11+
import java.util.List;
12+
import java.util.stream.Collectors;
13+
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
14+
import org.eclipse.microprofile.faulttolerance.Fallback;
15+
import org.eclipse.microprofile.faulttolerance.Retry;
16+
import org.eclipse.microprofile.faulttolerance.Timeout;
17+
import org.jboss.logging.Logger;
18+
19+
@ApplicationScoped
20+
public class AnalysisService {
21+
22+
private static final Logger LOG = Logger.getLogger(AnalysisService.class);
23+
24+
@Inject ProviderRegistry providerRegistry;
25+
26+
@CircuitBreaker(
27+
requestVolumeThreshold = 10,
28+
failureRatio = 0.5,
29+
successThreshold = 3,
30+
delay = 5000)
31+
@Retry(maxRetries = 3, delay = 1000)
32+
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
33+
public Uni<AIResponse> analyzeFailure(
34+
AnalysisResult analysisResult, AIProviderConfig providerConfig) {
35+
LOG.infof(
36+
"Starting AI analysis for analysis ID: %s using provider: %s",
37+
analysisResult.getAnalysisId(), providerConfig.getProviderId());
38+
39+
try {
40+
// get the AI provider implementation from ai-provider-lib
41+
AIProvider provider = providerRegistry.getProvider(providerConfig.getProviderId());
42+
43+
return provider.generateExplanation(analysisResult, providerConfig)
44+
.map(response -> enrichResponse(response, analysisResult))
45+
.onFailure()
46+
.invoke(
47+
throwable ->
48+
LOG.errorf(
49+
throwable,
50+
"AI provider call failed for provider: %s",
51+
providerConfig.getProviderId()));
52+
53+
} catch (Exception e) {
54+
LOG.errorf(e, "Failed to get AI provider: %s", providerConfig.getProviderId());
55+
return Uni.createFrom().failure(e);
56+
}
57+
}
58+
59+
@Fallback(fallbackMethod = "generateFallbackExplanation")
60+
public Uni<AIResponse> protectedAnalyzeFailure(
61+
AnalysisResult analysisResult, AIProviderConfig providerConfig) {
62+
return analyzeFailure(analysisResult, providerConfig);
63+
}
64+
65+
public Uni<AIResponse> generateFallbackExplanation(
66+
AnalysisResult analysisResult, AIProviderConfig providerConfig) {
67+
LOG.warnf("Using fallback explanation for analysis ID: %s", analysisResult.getAnalysisId());
68+
69+
// basic explanation based on analysis results when AI is unavailable
70+
String fallbackExplanation = buildBasicExplanation(analysisResult);
71+
72+
AIResponse fallbackResponse = new AIResponse();
73+
fallbackResponse.setExplanation(fallbackExplanation);
74+
fallbackResponse.setProviderId("fallback");
75+
fallbackResponse.setModelId("pattern-based");
76+
fallbackResponse.setGeneratedAt(Instant.now());
77+
fallbackResponse.setProcessingTime(Duration.ofMillis(100));
78+
fallbackResponse.setConfidence(0.6); // Lower confidence for fallback
79+
80+
return Uni.createFrom().item(fallbackResponse);
81+
}
82+
83+
public Uni<List<String>> getAvailableProviders() {
84+
return Uni.createFrom()
85+
.item(
86+
providerRegistry.getAllProviders().stream()
87+
.map(AIProvider::getProviderId)
88+
.collect(Collectors.toList()));
89+
}
90+
91+
public Uni<ValidationResult> validateProvider(AIProviderConfig config) {
92+
try {
93+
AIProvider provider = providerRegistry.getProvider(config.getProviderId());
94+
return provider.validateConfiguration(config);
95+
} catch (Exception e) {
96+
ValidationResult result = new ValidationResult();
97+
result.setValid(false);
98+
result.setProviderId(config.getProviderId());
99+
result.setMessage("Provider not found: " + e.getMessage());
100+
return Uni.createFrom().item(result);
101+
}
102+
}
103+
104+
private AIResponse enrichResponse(AIResponse response, AnalysisResult analysisResult) {
105+
// add any additional metadata or processing
106+
response.setGeneratedAt(Instant.now());
107+
108+
// add correlation with analysis metadata
109+
if (response.getMetadata() == null) {
110+
response.setMetadata(
111+
java.util.Map.of(
112+
"analysisId",
113+
analysisResult.getAnalysisId(),
114+
"eventCount",
115+
analysisResult.getEvents() != null
116+
? analysisResult.getEvents().size()
117+
: 0));
118+
} else {
119+
response.getMetadata().put("analysisId", analysisResult.getAnalysisId());
120+
response.getMetadata()
121+
.put(
122+
"eventCount",
123+
analysisResult.getEvents() != null
124+
? analysisResult.getEvents().size()
125+
: 0);
126+
}
127+
128+
return response;
129+
}
130+
131+
private String buildBasicExplanation(AnalysisResult analysisResult) {
132+
StringBuilder explanation = new StringBuilder();
133+
134+
explanation.append("Pod failure analysis (pattern-based fallback): ");
135+
136+
if (analysisResult.getEvents() != null && !analysisResult.getEvents().isEmpty()) {
137+
// Get the first critical event
138+
var firstEvent = analysisResult.getEvents().get(0);
139+
140+
// Access pattern ID and severity through the matched pattern
141+
if (firstEvent.getMatchedPattern() != null) {
142+
String patternId = firstEvent.getMatchedPattern().getId();
143+
String severity = firstEvent.getMatchedPattern().getSeverity();
144+
145+
explanation
146+
.append("The pod appears to have failed due to pattern '")
147+
.append(patternId != null ? patternId : "unknown")
148+
.append("' with severity ")
149+
.append(severity != null ? severity : "unknown")
150+
.append(". ");
151+
} else {
152+
explanation
153+
.append("The pod appears to have failed with score ")
154+
.append(firstEvent.getScore())
155+
.append(" at line ")
156+
.append(firstEvent.getLineNumber())
157+
.append(". ");
158+
}
159+
160+
if (analysisResult.getEvents().size() > 1) {
161+
explanation
162+
.append("Additional ")
163+
.append(analysisResult.getEvents().size() - 1)
164+
.append(" event(s) were also detected.");
165+
}
166+
} else {
167+
explanation.append("No specific failure patterns were detected in the log analysis.");
168+
}
169+
170+
return explanation.toString();
171+
}
172+
}

0 commit comments

Comments
 (0)