package artifacts import ( "context" "encoding/json" "fmt" "html/template" "sort" "strings" "time" "github.com/pkg/errors" analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze" "gopkg.in/yaml.v2" ) // JSONFormatter formats analysis results as JSON type JSONFormatter struct{} func (f *JSONFormatter) Format(ctx context.Context, result *analyzer.AnalysisResult) ([]byte, error) { return json.MarshalIndent(result, "", " ") } func (f *JSONFormatter) ContentType() string { return "application/json" } func (f *JSONFormatter) FileExtension() string { return "json" } // YAMLFormatter formats analysis results as YAML type YAMLFormatter struct{} func (f *YAMLFormatter) Format(ctx context.Context, result *analyzer.AnalysisResult) ([]byte, error) { return yaml.Marshal(result) } func (f *YAMLFormatter) ContentType() string { return "application/x-yaml" } func (f *YAMLFormatter) FileExtension() string { return "yaml" } // HTMLFormatter formats analysis results as HTML type HTMLFormatter struct{} func (f *HTMLFormatter) Format(ctx context.Context, result *analyzer.AnalysisResult) ([]byte, error) { tmpl := template.New("analysis").Funcs(template.FuncMap{ "formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") }, "statusIcon": func(r *analyzer.AnalyzerResult) string { if r.IsPass { return "✅" } else if r.IsWarn { return "⚠️" } else if r.IsFail { return "❌" } return "❓" }, "statusClass": func(r *analyzer.AnalyzerResult) string { if r.IsPass { return "success" } else if r.IsWarn { return "warning" } else if r.IsFail { return "danger" } return "info" }, "priorityBadge": func(priority int) string { if priority >= 8 { return "badge-danger" } else if priority >= 5 { return "badge-warning" } return "badge-info" }, "truncate": func(s string, length int) string { if len(s) <= length { return s } return s[:length] + "..." }, "mul": func(a, b float64) float64 { return a * b }, }) htmlTemplate := ` Analysis Report

Troubleshoot Analysis Report

Generated on {{formatTime .Metadata.Timestamp}} | Engine Version {{.Metadata.EngineVersion}}

{{.Summary.PassCount}}

Passed

{{.Summary.WarnCount}}

Warnings

{{.Summary.FailCount}}

Failed

{{.Summary.TotalAnalyzers}}

Total Analyzers

Analysis Results
{{range .Results}} {{end}}
Status Title Category Agent Confidence Message
{{statusIcon .}} {{.Title}} {{.Category}} {{.AgentName}} {{if .Confidence}}{{printf "%.1f%%" (mul .Confidence 100)}}{{else}}-{{end}} {{truncate .Message 100}}
Agents Used
{{range .Metadata.Agents}}
{{.Name}} {{.Duration}}
{{.ResultCount}} results, {{.ErrorCount}} errors

{{end}}
Summary Statistics

Duration: {{.Summary.Duration}}

{{if .Summary.Confidence}}

Confidence: {{printf "%.1f%%" (mul .Summary.Confidence 100)}}

{{end}}

Agents: {{len .Summary.AgentsUsed}}

Errors: {{len .Errors}}

{{if .Remediation}}
Remediation Steps
{{range .Remediation}}
{{.Description}}
Priority {{.Priority}}
{{if .Command}}

{{.Command}}

{{end}}
{{.Category}} {{if .IsAutomatable}}Automatable{{end}}
{{if .Documentation}}

Documentation

{{end}}
{{end}}
{{end}} {{if .Metadata.Correlations}}
Correlations and Insights
{{range .Metadata.Correlations}}
{{.Type}}

{{.Description}}

Confidence: {{printf "%.1f%%" (mul .Confidence 100)}}
{{end}}
{{end}} {{if .Errors}}
Analysis Errors
{{range .Errors}}
{{.Agent}}{{if .Analyzer}} - {{.Analyzer}}{{end}}: {{.Error}}
{{formatTime .Timestamp}}
{{end}}
{{end}}
` t, err := tmpl.Parse(htmlTemplate) if err != nil { return nil, errors.Wrap(err, "failed to parse HTML template") } var buf strings.Builder if err := t.Execute(&buf, result); err != nil { return nil, errors.Wrap(err, "failed to execute HTML template") } return []byte(buf.String()), nil } func (f *HTMLFormatter) ContentType() string { return "text/html" } func (f *HTMLFormatter) FileExtension() string { return "html" } // TextFormatter formats analysis results as plain text type TextFormatter struct{} func (f *TextFormatter) Format(ctx context.Context, result *analyzer.AnalysisResult) ([]byte, error) { var builder strings.Builder // Header builder.WriteString("TROUBLESHOOT ANALYSIS REPORT\n") builder.WriteString("===========================\n\n") // Timestamp builder.WriteString(fmt.Sprintf("Generated: %s\n", result.Metadata.Timestamp.Format("2006-01-02 15:04:05"))) builder.WriteString(fmt.Sprintf("Engine Version: %s\n", result.Metadata.EngineVersion)) builder.WriteString(fmt.Sprintf("Duration: %s\n\n", result.Summary.Duration)) // Summary builder.WriteString("SUMMARY\n") builder.WriteString("-------\n") builder.WriteString(fmt.Sprintf("Total Analyzers: %d\n", result.Summary.TotalAnalyzers)) builder.WriteString(fmt.Sprintf("Passed: %d\n", result.Summary.PassCount)) builder.WriteString(fmt.Sprintf("Warnings: %d\n", result.Summary.WarnCount)) builder.WriteString(fmt.Sprintf("Failed: %d\n", result.Summary.FailCount)) builder.WriteString(fmt.Sprintf("Errors: %d\n", result.Summary.ErrorCount)) if result.Summary.Confidence > 0 { builder.WriteString(fmt.Sprintf("Confidence: %.1f%%\n", result.Summary.Confidence*100)) } builder.WriteString(fmt.Sprintf("Agents Used: %s\n\n", strings.Join(result.Summary.AgentsUsed, ", "))) // Results builder.WriteString("ANALYSIS RESULTS\n") builder.WriteString("----------------\n\n") // Group results by status var passResults, warnResults, failResults []*analyzer.AnalyzerResult for _, r := range result.Results { if r.IsPass { passResults = append(passResults, r) } else if r.IsWarn { warnResults = append(warnResults, r) } else if r.IsFail { failResults = append(failResults, r) } } // Failed results first if len(failResults) > 0 { builder.WriteString("FAILED CHECKS:\n") for _, r := range failResults { f.writeResultText(&builder, r, "❌") } builder.WriteString("\n") } // Warning results if len(warnResults) > 0 { builder.WriteString("WARNING CHECKS:\n") for _, r := range warnResults { f.writeResultText(&builder, r, "⚠️") } builder.WriteString("\n") } // Passed results (summary only to save space) if len(passResults) > 0 { builder.WriteString(fmt.Sprintf("PASSED CHECKS: %d checks passed\n\n", len(passResults))) } // Remediation steps if len(result.Remediation) > 0 { builder.WriteString("REMEDIATION STEPS\n") builder.WriteString("-----------------\n\n") // Sort by priority remediation := make([]analyzer.RemediationStep, len(result.Remediation)) copy(remediation, result.Remediation) sort.Slice(remediation, func(i, j int) bool { return remediation[i].Priority > remediation[j].Priority }) for i, step := range remediation { builder.WriteString(fmt.Sprintf("%d. %s\n", i+1, step.Description)) builder.WriteString(fmt.Sprintf(" Category: %s | Priority: %d", step.Category, step.Priority)) if step.IsAutomatable { builder.WriteString(" | Automatable") } builder.WriteString("\n") if step.Command != "" { builder.WriteString(fmt.Sprintf(" Command: %s\n", step.Command)) } if step.Documentation != "" { builder.WriteString(fmt.Sprintf(" Documentation: %s\n", step.Documentation)) } builder.WriteString("\n") } } // Agent information if len(result.Metadata.Agents) > 0 { builder.WriteString("AGENT INFORMATION\n") builder.WriteString("-----------------\n") for _, agent := range result.Metadata.Agents { builder.WriteString(fmt.Sprintf("Agent: %s\n", agent.Name)) builder.WriteString(fmt.Sprintf(" Duration: %s\n", agent.Duration)) builder.WriteString(fmt.Sprintf(" Results: %d\n", agent.ResultCount)) builder.WriteString(fmt.Sprintf(" Errors: %d\n", agent.ErrorCount)) builder.WriteString(fmt.Sprintf(" Capabilities: %s\n\n", strings.Join(agent.Capabilities, ", "))) } } // Errors if len(result.Errors) > 0 { builder.WriteString("ANALYSIS ERRORS\n") builder.WriteString("---------------\n") for _, err := range result.Errors { builder.WriteString(fmt.Sprintf("• %s", err.Error)) if err.Agent != "" { builder.WriteString(fmt.Sprintf(" (Agent: %s)", err.Agent)) } if err.Analyzer != "" { builder.WriteString(fmt.Sprintf(" (Analyzer: %s)", err.Analyzer)) } builder.WriteString(fmt.Sprintf(" [%s]\n", err.Timestamp.Format("15:04:05"))) } builder.WriteString("\n") } return []byte(builder.String()), nil } func (f *TextFormatter) writeResultText(builder *strings.Builder, result *analyzer.AnalyzerResult, icon string) { builder.WriteString(fmt.Sprintf("%s %s", icon, result.Title)) if result.Category != "" { builder.WriteString(fmt.Sprintf(" [%s]", result.Category)) } builder.WriteString("\n") builder.WriteString(fmt.Sprintf(" %s", result.Message)) if result.AgentName != "" { builder.WriteString(fmt.Sprintf(" (via %s)", result.AgentName)) } if result.Confidence > 0 { builder.WriteString(fmt.Sprintf(" [%.0f%% confidence]", result.Confidence*100)) } builder.WriteString("\n") if len(result.Insights) > 0 { builder.WriteString(" Insights:\n") for _, insight := range result.Insights { builder.WriteString(fmt.Sprintf(" • %s\n", insight)) } } if result.Remediation != nil { builder.WriteString(fmt.Sprintf(" Remediation: %s\n", result.Remediation.Description)) if result.Remediation.Command != "" { builder.WriteString(fmt.Sprintf(" Command: %s\n", result.Remediation.Command)) } } builder.WriteString("\n") } func (f *TextFormatter) ContentType() string { return "text/plain" } func (f *TextFormatter) FileExtension() string { return "txt" }