Files
a8fb5210f4 Advanced analysis clean (#1868)
* created roadmap and yaml claude agent

* Update roadmap.md

* feat: Clean advanced analysis implementation - core agents, engine, artifacts

* Remove unrelated files - keep only advanced analysis implementation

* fix: Fix goroutine leak in hosted agent rate limiter

- Added stop channel and stopped flag to RateLimiter struct
- Modified replenishTokens to listen for stop signal and exit cleanly
- Added Stop() method to gracefully shutdown rate limiter
- Added Stop() method to HostedAgent to cleanup rate limiter on shutdown

Fixes cursor bot issue: Rate Limiter Goroutine Leak

* fix: Fix analyzer config and model validation bugs

Bug 1: Analyzer Config Missing File Path
- Added filePath to DeploymentStatus analyzer config in convertAnalyzerToSpec
- Sets namespace-specific path (cluster-resources/deployments/{namespace}.json)
- Falls back to generic path (cluster-resources/deployments.json) if no namespace
- Fixes LocalAgent.analyzeDeploymentStatus backward compatibility

Bug 2: HealthCheck Fails Model Validation
- Changed Ollama model validation from prefix match to exact match
- Prevents false positives where llama2:13b would match request for llama2:7b
- Ensures agent only reports healthy when exact model is available

Both fixes address cursor bot reported issues and maintain backward compatibility.

* fixing lint errors

* fixing lint errors

* adding CLI flags

* fix: resolve linting errors for CI

- Remove unnecessary nil check in host_kernel_configs.go (len() for nil slices is zero)
- Remove unnecessary fmt.Sprintf() calls in ceph.go for static strings
- Apply go fmt formatting fixes

Fixes failing lint CI check

* fix: resolve CI failures in build-test workflow and Ollama tests

1. Fix GitHub Actions workflow logic error:
   - Replace problematic contains() expression with explicit job result checks
   - Properly handle failure and cancelled states for each job
   - Prevents false positive failures in success summary job

2. Fix Ollama agent parseLLMResponse panics:
   - Add proper error handling for malformed JSON in LLM responses
   - Return error when JSON is found but invalid (instead of silent fallback)
   - Add error when no meaningful content can be parsed from response
   - Prevents nil pointer dereference in test assertions

Fixes failing build-test/success and build-test/test CI checks

* fix: resolve all CI failures and cursor bot issues

1. Fix disable-ollama flag logic bug:
   - Remove disable-ollama from advanced analysis trigger condition
   - Prevents unintended advanced analysis mode when no agents registered
   - Allows proper fallback to legacy analysis

2. Fix diff test consistency:
   - Update test expectations to match function behavior (lines with newlines)
   - Ensures consistency between streaming and non-streaming diff paths

3. Fix Ollama agent error handling:
   - Add proper error return for malformed JSON in LLM responses
   - Add meaningful content validation for markdown parsing
   - Prevents nil pointer panics in test assertions

4. Fix analysis engine mock agent:
   - Mock agent now processes and returns results for all provided analyzers
   - Fixes test expectation mismatch (expected 8 results, got 1)

Resolves all failing CI checks: lint, test, and success workflow logic

---------

Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com>
2025-09-30 11:44:46 -05:00

680 lines
18 KiB
Go

package artifacts
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
)
// SummaryGenerator generates summary artifacts
type SummaryGenerator struct{}
func (g *SummaryGenerator) Generate(ctx context.Context, result *analyzer.AnalysisResult) (*Artifact, error) {
summary := struct {
Overview analyzer.AnalysisSummary `json:"overview"`
TopIssues []*analyzer.AnalyzerResult `json:"topIssues"`
Categories map[string]int `json:"categories"`
Agents []analyzer.AgentMetadata `json:"agents"`
Recommendations []string `json:"recommendations"`
GeneratedAt time.Time `json:"generatedAt"`
}{
Overview: result.Summary,
Categories: g.categorizeResults(result.Results),
Agents: result.Metadata.Agents,
TopIssues: g.getTopIssues(result.Results, 10),
Recommendations: g.generateRecommendations(result),
GeneratedAt: time.Now(),
}
data, err := json.MarshalIndent(summary, "", " ")
if err != nil {
return nil, err
}
artifact := &Artifact{
Name: "summary.json",
Type: "summary",
Format: "json",
ContentType: "application/json",
Size: int64(len(data)),
Content: data,
Metadata: ArtifactMetadata{
CreatedAt: time.Now(),
Generator: "SummaryGenerator",
Version: "1.0.0",
Tags: []string{"summary", "overview"},
},
}
return artifact, nil
}
func (g *SummaryGenerator) Name() string {
return "Summary Generator"
}
func (g *SummaryGenerator) Description() string {
return "Generates high-level summary artifacts from analysis results"
}
func (g *SummaryGenerator) categorizeResults(results []*analyzer.AnalyzerResult) map[string]int {
categories := make(map[string]int)
for _, result := range results {
if result.Category != "" {
categories[result.Category]++
}
}
return categories
}
func (g *SummaryGenerator) getTopIssues(results []*analyzer.AnalyzerResult, limit int) []*analyzer.AnalyzerResult {
var failedResults []*analyzer.AnalyzerResult
for _, result := range results {
if result.IsFail {
failedResults = append(failedResults, result)
}
}
sort.Slice(failedResults, func(i, j int) bool {
return failedResults[i].Confidence > failedResults[j].Confidence
})
if len(failedResults) > limit {
return failedResults[:limit]
}
return failedResults
}
func (g *SummaryGenerator) generateRecommendations(result *analyzer.AnalysisResult) []string {
var recommendations []string
if result.Summary.FailCount > 0 {
recommendations = append(recommendations,
fmt.Sprintf("Address %d failed checks to improve system health", result.Summary.FailCount))
}
if result.Summary.WarnCount > result.Summary.PassCount {
recommendations = append(recommendations,
"Review warning conditions to prevent potential issues")
}
categories := g.categorizeResults(result.Results)
for category, count := range categories {
if count >= 5 {
recommendations = append(recommendations,
fmt.Sprintf("Focus attention on %s category (%d issues)", category, count))
}
}
return recommendations
}
// InsightsGenerator generates insights and correlation artifacts
type InsightsGenerator struct{}
func (g *InsightsGenerator) Generate(ctx context.Context, result *analyzer.AnalysisResult) (*Artifact, error) {
insights := struct {
KeyFindings []string `json:"keyFindings"`
Patterns []Pattern `json:"patterns"`
Correlations []analyzer.Correlation `json:"correlations"`
Trends []Trend `json:"trends"`
Recommendations []RemediationInsight `json:"recommendations"`
GeneratedAt time.Time `json:"generatedAt"`
}{
KeyFindings: g.extractKeyFindings(result.Results),
Patterns: g.identifyPatterns(result.Results),
Correlations: result.Metadata.Correlations,
Trends: g.analyzeTrends(result.Results),
Recommendations: g.generateRemediationInsights(result.Remediation),
GeneratedAt: time.Now(),
}
data, err := json.MarshalIndent(insights, "", " ")
if err != nil {
return nil, err
}
artifact := &Artifact{
Name: "insights.json",
Type: "insights",
Format: "json",
ContentType: "application/json",
Size: int64(len(data)),
Content: data,
Metadata: ArtifactMetadata{
CreatedAt: time.Now(),
Generator: "InsightsGenerator",
Version: "1.0.0",
Tags: []string{"insights", "patterns", "correlations"},
},
}
return artifact, nil
}
func (g *InsightsGenerator) Name() string {
return "Insights Generator"
}
func (g *InsightsGenerator) Description() string {
return "Generates insights, patterns, and correlation artifacts"
}
func (g *InsightsGenerator) extractKeyFindings(results []*analyzer.AnalyzerResult) []string {
var findings []string
for _, result := range results {
if result.IsFail && result.Confidence > 0.8 {
findings = append(findings, result.Message)
}
}
if len(findings) > 10 {
findings = findings[:10]
}
return findings
}
func (g *InsightsGenerator) identifyPatterns(results []*analyzer.AnalyzerResult) []Pattern {
var patterns []Pattern
// Pattern: Multiple failures in same category
categoryFailures := make(map[string]int)
for _, result := range results {
if result.IsFail && result.Category != "" {
categoryFailures[result.Category]++
}
}
for category, count := range categoryFailures {
if count >= 3 {
patterns = append(patterns, Pattern{
Type: "category-failure-cluster",
Description: fmt.Sprintf("Multiple failures in %s category", category),
Count: count,
Confidence: 0.8,
})
}
}
// Pattern: Agent-specific issues
agentFailures := make(map[string]int)
for _, result := range results {
if result.IsFail && result.AgentName != "" {
agentFailures[result.AgentName]++
}
}
for agent, count := range agentFailures {
if count >= 2 {
patterns = append(patterns, Pattern{
Type: "agent-failure-pattern",
Description: fmt.Sprintf("Multiple failures detected by %s agent", agent),
Count: count,
Confidence: 0.7,
})
}
}
return patterns
}
func (g *InsightsGenerator) analyzeTrends(results []*analyzer.AnalyzerResult) []Trend {
// Placeholder for trend analysis
// In a real implementation, this would compare with historical data
totalResults := len(results)
failedResults := 0
for _, result := range results {
if result.IsFail {
failedResults++
}
}
var direction string
var confidence float64
failureRate := float64(failedResults) / float64(totalResults)
if failureRate < 0.1 {
direction = "stable"
confidence = 0.8
} else if failureRate < 0.3 {
direction = "stable"
confidence = 0.6
} else {
direction = "degrading"
confidence = 0.7
}
return []Trend{
{
Category: "overall",
Direction: direction,
Confidence: confidence,
Description: fmt.Sprintf("System health appears %s based on current analysis", direction),
},
}
}
func (g *InsightsGenerator) generateRemediationInsights(steps []analyzer.RemediationStep) []RemediationInsight {
var insights []RemediationInsight
// Group by category and generate insights
categories := make(map[string][]analyzer.RemediationStep)
for _, step := range steps {
category := step.Category
if category == "" {
category = "general"
}
categories[category] = append(categories[category], step)
}
for category, categorySteps := range categories {
highPriorityCount := 0
automatableCount := 0
for _, step := range categorySteps {
if step.Priority >= 8 {
highPriorityCount++
}
if step.IsAutomatable {
automatableCount++
}
}
var impact, effort string
if highPriorityCount > len(categorySteps)/2 {
impact = "high"
} else {
impact = "medium"
}
if automatableCount > len(categorySteps)/2 {
effort = "low"
} else {
effort = "medium"
}
insights = append(insights, RemediationInsight{
Category: category,
Priority: highPriorityCount,
Impact: impact,
Effort: effort,
Description: fmt.Sprintf("%d steps in %s category, %d high priority",
len(categorySteps), category, highPriorityCount),
})
}
return insights
}
// RemediationGenerator generates remediation guide artifacts
type RemediationGenerator struct{}
func (g *RemediationGenerator) Generate(ctx context.Context, result *analyzer.AnalysisResult) (*Artifact, error) {
guide := struct {
Summary string `json:"summary"`
PriorityActions []analyzer.RemediationStep `json:"priorityActions"`
Categories map[string][]analyzer.RemediationStep `json:"categories"`
Prerequisites []string `json:"prerequisites"`
Automation AutomationGuide `json:"automation"`
GeneratedAt time.Time `json:"generatedAt"`
}{
Summary: g.generateRemediationSummary(result.Remediation),
PriorityActions: g.getPriorityActions(result.Remediation, 5),
Categories: g.categorizeRemediationSteps(result.Remediation),
Prerequisites: g.identifyPrerequisites(result.Remediation),
Automation: g.generateAutomationGuide(result.Remediation),
GeneratedAt: time.Now(),
}
data, err := json.MarshalIndent(guide, "", " ")
if err != nil {
return nil, err
}
artifact := &Artifact{
Name: "remediation-guide.json",
Type: "remediation",
Format: "json",
ContentType: "application/json",
Size: int64(len(data)),
Content: data,
Metadata: ArtifactMetadata{
CreatedAt: time.Now(),
Generator: "RemediationGenerator",
Version: "1.0.0",
Tags: []string{"remediation", "guide", "actions"},
},
}
return artifact, nil
}
func (g *RemediationGenerator) Name() string {
return "Remediation Generator"
}
func (g *RemediationGenerator) Description() string {
return "Generates detailed remediation guide artifacts"
}
func (g *RemediationGenerator) generateRemediationSummary(steps []analyzer.RemediationStep) string {
if len(steps) == 0 {
return "No remediation steps required"
}
automatable := 0
highPriority := 0
for _, step := range steps {
if step.IsAutomatable {
automatable++
}
if step.Priority >= 8 {
highPriority++
}
}
return fmt.Sprintf("Found %d remediation steps: %d high priority, %d automatable",
len(steps), highPriority, automatable)
}
func (g *RemediationGenerator) getPriorityActions(steps []analyzer.RemediationStep, limit int) []analyzer.RemediationStep {
sorted := make([]analyzer.RemediationStep, len(steps))
copy(sorted, steps)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Priority > sorted[j].Priority
})
if len(sorted) > limit {
return sorted[:limit]
}
return sorted
}
func (g *RemediationGenerator) categorizeRemediationSteps(steps []analyzer.RemediationStep) map[string][]analyzer.RemediationStep {
categories := make(map[string][]analyzer.RemediationStep)
for _, step := range steps {
category := step.Category
if category == "" {
category = "general"
}
categories[category] = append(categories[category], step)
}
return categories
}
func (g *RemediationGenerator) identifyPrerequisites(steps []analyzer.RemediationStep) []string {
var prerequisites []string
categoryPrereqs := map[string]string{
"infrastructure": "Admin access to cluster nodes",
"networking": "Network configuration permissions",
"storage": "Storage admin permissions",
"security": "Security policy modification rights",
}
categories := make(map[string]bool)
for _, step := range steps {
if step.Category != "" {
categories[step.Category] = true
}
}
for category := range categories {
if prereq, exists := categoryPrereqs[category]; exists {
prerequisites = append(prerequisites, prereq)
}
}
return prerequisites
}
func (g *RemediationGenerator) generateAutomationGuide(steps []analyzer.RemediationStep) AutomationGuide {
automatable := 0
manual := 0
for _, step := range steps {
if step.IsAutomatable {
automatable++
} else {
manual++
}
}
var scripts []Script
if automatable > 0 {
scripts = append(scripts, Script{
Name: "automated-remediation.sh",
Description: "Automated remediation script for detected issues",
Language: "bash",
Content: g.generateRemediationScript(steps),
Prerequisites: []string{"kubectl", "admin access", "bash"},
})
}
return AutomationGuide{
AutomatableSteps: automatable,
ManualSteps: manual,
Scripts: scripts,
}
}
func (g *RemediationGenerator) generateRemediationScript(steps []analyzer.RemediationStep) string {
script := `#!/bin/bash
# Automated Remediation Script
# Generated by Troubleshoot Analysis Engine
set -e
echo "Starting automated remediation..."
`
for i, step := range steps {
if step.IsAutomatable && step.Command != "" {
script += fmt.Sprintf(`
# Step %d: %s
echo "Executing: %s"
if %s; then
echo "✅ Step %d completed successfully"
else
echo "❌ Step %d failed - manual intervention required"
fi
`, i+1, step.Description, step.Description, step.Command, i+1, i+1)
}
}
script += `
echo "Automated remediation completed. Please review any failed steps manually."
`
return script
}
// CorrelationGenerator generates correlation matrix artifacts
type CorrelationGenerator struct{}
func (g *CorrelationGenerator) Generate(ctx context.Context, result *analyzer.AnalysisResult) (*Artifact, error) {
correlations := g.buildCorrelationMatrix(result.Results)
data, err := json.MarshalIndent(correlations, "", " ")
if err != nil {
return nil, err
}
artifact := &Artifact{
Name: "correlations.json",
Type: "correlations",
Format: "json",
ContentType: "application/json",
Size: int64(len(data)),
Content: data,
Metadata: ArtifactMetadata{
CreatedAt: time.Now(),
Generator: "CorrelationGenerator",
Version: "1.0.0",
Tags: []string{"correlations", "relationships"},
},
}
return artifact, nil
}
func (g *CorrelationGenerator) Name() string {
return "Correlation Generator"
}
func (g *CorrelationGenerator) Description() string {
return "Generates correlation matrix and relationship artifacts"
}
func (g *CorrelationGenerator) buildCorrelationMatrix(results []*analyzer.AnalyzerResult) map[string]interface{} {
correlations := make(map[string]interface{})
// Namespace-based correlations
namespaceFailures := make(map[string][]string)
namespaceWarnings := make(map[string][]string)
for _, result := range results {
if result.InvolvedObject != nil && result.InvolvedObject.Namespace != "" {
namespace := result.InvolvedObject.Namespace
if result.IsFail {
namespaceFailures[namespace] = append(namespaceFailures[namespace], result.Title)
} else if result.IsWarn {
namespaceWarnings[namespace] = append(namespaceWarnings[namespace], result.Title)
}
}
}
correlations["namespace_failures"] = namespaceFailures
correlations["namespace_warnings"] = namespaceWarnings
// Category-based correlations
categoryCorrelations := make(map[string]map[string]int)
for _, result := range results {
if result.Category != "" {
if categoryCorrelations[result.Category] == nil {
categoryCorrelations[result.Category] = make(map[string]int)
}
status := "unknown"
if result.IsPass {
status = "pass"
} else if result.IsWarn {
status = "warn"
} else if result.IsFail {
status = "fail"
}
categoryCorrelations[result.Category][status]++
}
}
correlations["category_status_distribution"] = categoryCorrelations
// Agent-based correlations
agentResults := make(map[string]map[string]int)
for _, result := range results {
if result.AgentName != "" {
if agentResults[result.AgentName] == nil {
agentResults[result.AgentName] = make(map[string]int)
}
if result.IsPass {
agentResults[result.AgentName]["pass"]++
} else if result.IsWarn {
agentResults[result.AgentName]["warn"]++
} else if result.IsFail {
agentResults[result.AgentName]["fail"]++
}
}
}
correlations["agent_performance"] = agentResults
// Confidence correlations
confidenceRanges := map[string]int{
"high (>0.8)": 0,
"medium (0.5-0.8)": 0,
"low (<0.5)": 0,
"unspecified": 0,
}
for _, result := range results {
if result.Confidence > 0.8 {
confidenceRanges["high (>0.8)"]++
} else if result.Confidence > 0.5 {
confidenceRanges["medium (0.5-0.8)"]++
} else if result.Confidence > 0 {
confidenceRanges["low (<0.5)"]++
} else {
confidenceRanges["unspecified"]++
}
}
correlations["confidence_distribution"] = confidenceRanges
return correlations
}
// GeneratorRegistry manages all artifact generators
type GeneratorRegistry struct {
generators map[string]ArtifactGenerator
}
// NewGeneratorRegistry creates a new generator registry
func NewGeneratorRegistry() *GeneratorRegistry {
registry := &GeneratorRegistry{
generators: make(map[string]ArtifactGenerator),
}
// Register default generators
registry.RegisterGenerator("summary", &SummaryGenerator{})
registry.RegisterGenerator("insights", &InsightsGenerator{})
registry.RegisterGenerator("remediation", &RemediationGenerator{})
registry.RegisterGenerator("correlations", &CorrelationGenerator{})
return registry
}
// RegisterGenerator registers a new generator
func (r *GeneratorRegistry) RegisterGenerator(name string, generator ArtifactGenerator) {
r.generators[name] = generator
}
// GetGenerator gets a generator by name
func (r *GeneratorRegistry) GetGenerator(name string) (ArtifactGenerator, bool) {
generator, exists := r.generators[name]
return generator, exists
}
// GenerateArtifact generates an artifact using the specified generator
func (r *GeneratorRegistry) GenerateArtifact(ctx context.Context, generatorName string, result *analyzer.AnalysisResult) (*Artifact, error) {
generator, exists := r.GetGenerator(generatorName)
if !exists {
return nil, fmt.Errorf("no generator found with name: %s", generatorName)
}
return generator.Generate(ctx, result)
}
// ListGenerators returns all available generator names
func (r *GeneratorRegistry) ListGenerators() []string {
var names []string
for name := range r.generators {
names = append(names, name)
}
sort.Strings(names)
return names
}