Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
name: Test Build on Pull Request
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
34 changes: 31 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.3</surefire-plugin.version>

<podmortem.common.lib.version>1.0-f3f9123-SNAPSHOT</podmortem.common.lib.version>
<podmortem.common.lib.version>1.0-35533c7-SNAPSHOT</podmortem.common.lib.version>
<podmortem.ai.provider.lib.version>1.0-073da55-SNAPSHOT</podmortem.ai.provider.lib.version>
</properties>

<repositories>
<repository>
<id>github</id>
<name>GitHub podmortem Apache Maven Packages</name>
<id>github-common</id>
<name>Common Library</name>
<url>https://maven.pkg.github.com/podmortem/common-lib</url>
</repository>
<repository>
<id>github-ai-provider</id>
<name>AI Provider Library</name>
<url>https://maven.pkg.github.com/podmortem/ai-provider-lib</url>
</repository>
</repositories>

<dependencyManagement>
Expand All @@ -48,11 +54,33 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.redhat.podmortem</groupId>
<artifactId>common</artifactId>
<version>${podmortem.common.lib.version}</version>
</dependency>
<dependency>
<groupId>com.redhat.podmortem</groupId>
<artifactId>provider</artifactId>
<version>${podmortem.ai.provider.lib.version}</version>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
Expand Down
16 changes: 0 additions & 16 deletions src/main/java/com/redhat/podmortem/GreetingResource.java

This file was deleted.

90 changes: 90 additions & 0 deletions src/main/java/com/redhat/podmortem/ai/rest/Analysis.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.redhat.podmortem.ai.rest;

import com.redhat.podmortem.ai.service.AnalysisService;
import com.redhat.podmortem.common.model.analysis.AnalysisRequest;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Map;
import org.jboss.logging.Logger;

@Path("/api/v1/ai-analysis")
@ApplicationScoped
public class Analysis {

private static final Logger LOG = Logger.getLogger(Analysis.class);

@Inject AnalysisService aiAnalysisService;

@POST
@Path("/explain")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Uni<Response> explainFailure(AnalysisRequest request) {
LOG.infof(
"Received AI analysis request for analysis ID: %s",
request.getAnalysisResult().getAnalysisId());

return aiAnalysisService
.analyzeFailure(request.getAnalysisResult(), request.getProviderConfig())
.map(
response -> {
LOG.infof(
"AI analysis completed for analysis ID: %s",
request.getAnalysisResult().getAnalysisId());
return Response.ok(response).build();
})
.onFailure()
.recoverWithItem(
throwable -> {
LOG.errorf(
throwable,
"AI analysis failed for analysis ID: %s",
request.getAnalysisResult().getAnalysisId());
return Response.status(500)
.entity(
Map.of(
"error", "AI analysis failed",
"message", throwable.getMessage(),
"analysisId",
request.getAnalysisResult()
.getAnalysisId()))
.build();
});
}

@GET
@Path("/health")
@Produces(MediaType.APPLICATION_JSON)
public Response healthCheck() {
return Response.ok(
Map.of(
"status", "UP",
"service", "ai-interface",
"timestamp", System.currentTimeMillis()))
.build();
}

@GET
@Path("/providers")
@Produces(MediaType.APPLICATION_JSON)
public Uni<Response> listProviders() {
return aiAnalysisService
.getAvailableProviders()
.map(providers -> Response.ok(Map.of("providers", providers)).build())
.onFailure()
.recoverWithItem(
throwable ->
Response.status(500)
.entity(
Map.of(
"error",
"Failed to list providers",
"message",
throwable.getMessage()))
.build());
}
}
172 changes: 172 additions & 0 deletions src/main/java/com/redhat/podmortem/ai/service/AnalysisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package com.redhat.podmortem.ai.service;

import com.redhat.podmortem.common.model.analysis.AnalysisResult;
import com.redhat.podmortem.common.model.provider.*;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.eclipse.microprofile.faulttolerance.Fallback;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.faulttolerance.Timeout;
import org.jboss.logging.Logger;

@ApplicationScoped
public class AnalysisService {

private static final Logger LOG = Logger.getLogger(AnalysisService.class);

@Inject ProviderRegistry providerRegistry;

@CircuitBreaker(
requestVolumeThreshold = 10,
failureRatio = 0.5,
successThreshold = 3,
delay = 5000)
@Retry(maxRetries = 3, delay = 1000)
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
public Uni<AIResponse> analyzeFailure(
AnalysisResult analysisResult, AIProviderConfig providerConfig) {
LOG.infof(
"Starting AI analysis for analysis ID: %s using provider: %s",
analysisResult.getAnalysisId(), providerConfig.getProviderId());

try {
// get the AI provider implementation from ai-provider-lib
AIProvider provider = providerRegistry.getProvider(providerConfig.getProviderId());

return provider.generateExplanation(analysisResult, providerConfig)
.map(response -> enrichResponse(response, analysisResult))
.onFailure()
.invoke(
throwable ->
LOG.errorf(
throwable,
"AI provider call failed for provider: %s",
providerConfig.getProviderId()));

} catch (Exception e) {
LOG.errorf(e, "Failed to get AI provider: %s", providerConfig.getProviderId());
return Uni.createFrom().failure(e);
}
}

@Fallback(fallbackMethod = "generateFallbackExplanation")
public Uni<AIResponse> protectedAnalyzeFailure(
AnalysisResult analysisResult, AIProviderConfig providerConfig) {
return analyzeFailure(analysisResult, providerConfig);
}

public Uni<AIResponse> generateFallbackExplanation(
AnalysisResult analysisResult, AIProviderConfig providerConfig) {
LOG.warnf("Using fallback explanation for analysis ID: %s", analysisResult.getAnalysisId());

// basic explanation based on analysis results when AI is unavailable
String fallbackExplanation = buildBasicExplanation(analysisResult);

AIResponse fallbackResponse = new AIResponse();
fallbackResponse.setExplanation(fallbackExplanation);
fallbackResponse.setProviderId("fallback");
fallbackResponse.setModelId("pattern-based");
fallbackResponse.setGeneratedAt(Instant.now());
fallbackResponse.setProcessingTime(Duration.ofMillis(100));
fallbackResponse.setConfidence(0.6); // Lower confidence for fallback

return Uni.createFrom().item(fallbackResponse);
}

public Uni<List<String>> getAvailableProviders() {
return Uni.createFrom()
.item(
providerRegistry.getAllProviders().stream()
.map(AIProvider::getProviderId)
.collect(Collectors.toList()));
}

public Uni<ValidationResult> validateProvider(AIProviderConfig config) {
try {
AIProvider provider = providerRegistry.getProvider(config.getProviderId());
return provider.validateConfiguration(config);
} catch (Exception e) {
ValidationResult result = new ValidationResult();
result.setValid(false);
result.setProviderId(config.getProviderId());
result.setMessage("Provider not found: " + e.getMessage());
return Uni.createFrom().item(result);
}
}

private AIResponse enrichResponse(AIResponse response, AnalysisResult analysisResult) {
// add any additional metadata or processing
response.setGeneratedAt(Instant.now());

// add correlation with analysis metadata
if (response.getMetadata() == null) {
response.setMetadata(
java.util.Map.of(
"analysisId",
analysisResult.getAnalysisId(),
"eventCount",
analysisResult.getEvents() != null
? analysisResult.getEvents().size()
: 0));
} else {
response.getMetadata().put("analysisId", analysisResult.getAnalysisId());
response.getMetadata()
.put(
"eventCount",
analysisResult.getEvents() != null
? analysisResult.getEvents().size()
: 0);
}

return response;
}

private String buildBasicExplanation(AnalysisResult analysisResult) {
StringBuilder explanation = new StringBuilder();

explanation.append("Pod failure analysis (pattern-based fallback): ");

if (analysisResult.getEvents() != null && !analysisResult.getEvents().isEmpty()) {
// Get the first critical event
var firstEvent = analysisResult.getEvents().get(0);

// Access pattern ID and severity through the matched pattern
if (firstEvent.getMatchedPattern() != null) {
String patternId = firstEvent.getMatchedPattern().getId();
String severity = firstEvent.getMatchedPattern().getSeverity();

explanation
.append("The pod appears to have failed due to pattern '")
.append(patternId != null ? patternId : "unknown")
.append("' with severity ")
.append(severity != null ? severity : "unknown")
.append(". ");
} else {
explanation
.append("The pod appears to have failed with score ")
.append(firstEvent.getScore())
.append(" at line ")
.append(firstEvent.getLineNumber())
.append(". ");
}

if (analysisResult.getEvents().size() > 1) {
explanation
.append("Additional ")
.append(analysisResult.getEvents().size() - 1)
.append(" event(s) were also detected.");
}
} else {
explanation.append("No specific failure patterns were detected in the log analysis.");
}

return explanation.toString();
}
}
Loading
Loading