Files
dbbackup/internal/report/output.go
Alexander Renz dbb0f6f942 feat(engine): physical backup revolution - XtraBackup capabilities in pure Go
Why wrap external tools when you can BE the tool?

New physical backup engines:
• MySQL Clone Plugin - native 8.0.17+ physical backup
• Filesystem Snapshots - LVM/ZFS/Btrfs orchestration
• Binlog Streaming - continuous backup with seconds RPO
• Parallel Cloud Upload - stream directly to S3, skip local disk

Smart engine selection automatically picks the optimal strategy based on:
- MySQL version and edition
- Available filesystem features
- Database size
- Cloud connectivity

Zero external dependencies. Single binary. Enterprise capabilities.

Commercial backup vendors: we need to talk.
2025-12-13 21:21:17 +01:00

545 lines
15 KiB
Go

// Package report - Output formatters
package report
import (
"encoding/json"
"fmt"
"io"
"strings"
"text/template"
"time"
)
// Formatter formats reports for output
type Formatter interface {
Format(report *Report, w io.Writer) error
}
// JSONFormatter formats reports as JSON
type JSONFormatter struct {
Indent bool
}
// Format writes the report as JSON
func (f *JSONFormatter) Format(report *Report, w io.Writer) error {
var data []byte
var err error
if f.Indent {
data, err = json.MarshalIndent(report, "", " ")
} else {
data, err = json.Marshal(report)
}
if err != nil {
return err
}
_, err = w.Write(data)
return err
}
// MarkdownFormatter formats reports as Markdown
type MarkdownFormatter struct{}
// Format writes the report as Markdown
func (f *MarkdownFormatter) Format(report *Report, w io.Writer) error {
tmpl := template.Must(template.New("report").Funcs(template.FuncMap{
"statusIcon": StatusIcon,
"severityIcon": SeverityIcon,
"formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") },
"formatDate": func(t time.Time) string { return t.Format("2006-01-02") },
"upper": strings.ToUpper,
}).Parse(markdownTemplate))
return tmpl.Execute(w, report)
}
const markdownTemplate = `# {{.Title}}
**Generated:** {{formatTime .GeneratedAt}}
**Period:** {{formatDate .PeriodStart}} to {{formatDate .PeriodEnd}}
**Overall Status:** {{statusIcon .Status}} {{.Status}}
**Compliance Score:** {{printf "%.1f" .Score}}%
---
## Executive Summary
{{.Description}}
| Metric | Value |
|--------|-------|
| Total Controls | {{.Summary.TotalControls}} |
| Compliant | {{.Summary.CompliantControls}} |
| Non-Compliant | {{.Summary.NonCompliantControls}} |
| Partial | {{.Summary.PartialControls}} |
| Compliance Rate | {{printf "%.1f" .Summary.ComplianceRate}}% |
| Open Findings | {{.Summary.OpenFindings}} |
| Risk Score | {{printf "%.1f" .Summary.RiskScore}} |
---
## Compliance Categories
{{range .Categories}}
### {{statusIcon .Status}} {{.Name}}
**Status:** {{.Status}} | **Score:** {{printf "%.1f" .Score}}%
{{.Description}}
| Control | Reference | Status | Notes |
|---------|-----------|--------|-------|
{{range .Controls}}| {{.Name}} | {{.Reference}} | {{statusIcon .Status}} | {{.Notes}} |
{{end}}
{{end}}
---
## Findings
{{if .Findings}}
| ID | Severity | Title | Status |
|----|----------|-------|--------|
{{range .Findings}}| {{.ID}} | {{severityIcon .Severity}} {{.Severity}} | {{.Title}} | {{.Status}} |
{{end}}
### Finding Details
{{range .Findings}}
#### {{severityIcon .Severity}} {{.Title}}
- **ID:** {{.ID}}
- **Control:** {{.ControlID}}
- **Severity:** {{.Severity}}
- **Type:** {{.Type}}
- **Status:** {{.Status}}
- **Detected:** {{formatTime .DetectedAt}}
**Description:** {{.Description}}
**Impact:** {{.Impact}}
**Recommendation:** {{.Recommendation}}
---
{{end}}
{{else}}
No open findings.
{{end}}
---
## Evidence Summary
{{if .Evidence}}
| ID | Type | Description | Collected |
|----|------|-------------|-----------|
{{range .Evidence}}| {{.ID}} | {{.Type}} | {{.Description}} | {{formatTime .CollectedAt}} |
{{end}}
{{else}}
No evidence collected.
{{end}}
---
*Report generated by dbbackup compliance module*
`
// HTMLFormatter formats reports as HTML
type HTMLFormatter struct{}
// Format writes the report as HTML
func (f *HTMLFormatter) Format(report *Report, w io.Writer) error {
tmpl := template.Must(template.New("report").Funcs(template.FuncMap{
"statusIcon": StatusIcon,
"statusClass": statusClass,
"severityIcon": SeverityIcon,
"severityClass": severityClass,
"formatTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") },
"formatDate": func(t time.Time) string { return t.Format("2006-01-02") },
}).Parse(htmlTemplate))
return tmpl.Execute(w, report)
}
func statusClass(s ComplianceStatus) string {
switch s {
case StatusCompliant:
return "status-compliant"
case StatusNonCompliant:
return "status-noncompliant"
case StatusPartial:
return "status-partial"
default:
return "status-unknown"
}
}
func severityClass(s FindingSeverity) string {
switch s {
case SeverityCritical:
return "severity-critical"
case SeverityHigh:
return "severity-high"
case SeverityMedium:
return "severity-medium"
case SeverityLow:
return "severity-low"
default:
return "severity-unknown"
}
}
const htmlTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<style>
:root {
--color-compliant: #27ae60;
--color-noncompliant: #e74c3c;
--color-partial: #f39c12;
--color-unknown: #95a5a6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.report-header {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.report-header h1 {
margin: 0 0 10px 0;
color: #2c3e50;
}
.report-meta {
color: #7f8c8d;
font-size: 0.9em;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-weight: bold;
font-size: 0.85em;
}
.status-compliant { background: var(--color-compliant); color: white; }
.status-noncompliant { background: var(--color-noncompliant); color: white; }
.status-partial { background: var(--color-partial); color: white; }
.status-unknown { background: var(--color-unknown); color: white; }
.score-display {
font-size: 48px;
font-weight: bold;
color: #2c3e50;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin: 20px 0;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.summary-card .value {
font-size: 32px;
font-weight: bold;
color: #2c3e50;
}
.summary-card .label {
color: #7f8c8d;
font-size: 0.9em;
}
.section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.section h2 {
margin-top: 0;
color: #2c3e50;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.category {
margin-bottom: 30px;
}
.category h3 {
display: flex;
align-items: center;
gap: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
th {
background: #f8f9fa;
font-weight: 600;
}
tr:hover {
background: #f8f9fa;
}
.finding-card {
border-left: 4px solid var(--color-unknown);
padding: 15px;
margin: 15px 0;
background: #f8f9fa;
border-radius: 0 8px 8px 0;
}
.finding-card.severity-critical { border-color: #e74c3c; }
.finding-card.severity-high { border-color: #e67e22; }
.finding-card.severity-medium { border-color: #f39c12; }
.finding-card.severity-low { border-color: #27ae60; }
.finding-card h4 {
margin: 0 0 10px 0;
}
.finding-meta {
font-size: 0.85em;
color: #7f8c8d;
}
.progress-bar {
height: 8px;
background: #ecf0f1;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-compliant);
transition: width 0.3s ease;
}
.footer {
text-align: center;
color: #7f8c8d;
padding: 20px;
font-size: 0.85em;
}
</style>
</head>
<body>
<div class="report-header">
<h1>{{.Title}}</h1>
<p class="report-meta">
Generated: {{formatTime .GeneratedAt}} |
Period: {{formatDate .PeriodStart}} to {{formatDate .PeriodEnd}}
</p>
<div style="display: flex; align-items: center; gap: 20px; margin-top: 20px;">
<div class="score-display">{{printf "%.0f" .Score}}%</div>
<div>
<span class="status-badge {{statusClass .Status}}">{{.Status}}</span>
<p style="margin: 10px 0 0 0; color: #7f8c8d;">{{.Description}}</p>
</div>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="value">{{.Summary.TotalControls}}</div>
<div class="label">Total Controls</div>
</div>
<div class="summary-card">
<div class="value" style="color: var(--color-compliant);">{{.Summary.CompliantControls}}</div>
<div class="label">Compliant</div>
</div>
<div class="summary-card">
<div class="value" style="color: var(--color-noncompliant);">{{.Summary.NonCompliantControls}}</div>
<div class="label">Non-Compliant</div>
</div>
<div class="summary-card">
<div class="value" style="color: var(--color-partial);">{{.Summary.PartialControls}}</div>
<div class="label">Partial</div>
</div>
<div class="summary-card">
<div class="value">{{.Summary.OpenFindings}}</div>
<div class="label">Open Findings</div>
</div>
<div class="summary-card">
<div class="value">{{printf "%.1f" .Summary.RiskScore}}</div>
<div class="label">Risk Score</div>
</div>
</div>
<div class="section">
<h2>Compliance Categories</h2>
{{range .Categories}}
<div class="category">
<h3>
{{statusIcon .Status}} {{.Name}}
<span class="status-badge {{statusClass .Status}}">{{printf "%.0f" .Score}}%</span>
</h3>
<p style="color: #7f8c8d;">{{.Description}}</p>
<div class="progress-bar">
<div class="progress-fill" style="width: {{printf "%.0f" .Score}}%;"></div>
</div>
<table>
<thead>
<tr>
<th>Control</th>
<th>Reference</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{{range .Controls}}
<tr>
<td>{{.Name}}</td>
<td>{{.Reference}}</td>
<td>{{statusIcon .Status}}</td>
<td>{{.Notes}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
{{if .Findings}}
<div class="section">
<h2>Findings ({{len .Findings}})</h2>
{{range .Findings}}
<div class="finding-card {{severityClass .Severity}}">
<h4>{{severityIcon .Severity}} {{.Title}}</h4>
<div class="finding-meta">
<strong>ID:</strong> {{.ID}} |
<strong>Severity:</strong> {{.Severity}} |
<strong>Status:</strong> {{.Status}} |
<strong>Detected:</strong> {{formatTime .DetectedAt}}
</div>
<p><strong>Description:</strong> {{.Description}}</p>
<p><strong>Impact:</strong> {{.Impact}}</p>
<p><strong>Recommendation:</strong> {{.Recommendation}}</p>
</div>
{{end}}
</div>
{{end}}
{{if .Evidence}}
<div class="section">
<h2>Evidence ({{len .Evidence}} items)</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Description</th>
<th>Collected</th>
</tr>
</thead>
<tbody>
{{range .Evidence}}
<tr>
<td>{{.ID}}</td>
<td>{{.Type}}</td>
<td>{{.Description}}</td>
<td>{{formatTime .CollectedAt}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<div class="footer">
<p>Report generated by dbbackup compliance module</p>
<p>Report ID: {{.ID}}</p>
</div>
</body>
</html>`
// GetFormatter returns a formatter for the given format
func GetFormatter(format OutputFormat) Formatter {
switch format {
case FormatJSON:
return &JSONFormatter{Indent: true}
case FormatMarkdown:
return &MarkdownFormatter{}
case FormatHTML:
return &HTMLFormatter{}
default:
return &JSONFormatter{Indent: true}
}
}
// ConsoleFormatter formats reports for terminal output
type ConsoleFormatter struct{}
// Format writes the report to console
func (f *ConsoleFormatter) Format(report *Report, w io.Writer) error {
// Header
fmt.Fprintf(w, "\n%s\n", strings.Repeat("=", 60))
fmt.Fprintf(w, " %s\n", report.Title)
fmt.Fprintf(w, "%s\n\n", strings.Repeat("=", 60))
fmt.Fprintf(w, " Generated: %s\n", report.GeneratedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(w, " Period: %s to %s\n",
report.PeriodStart.Format("2006-01-02"),
report.PeriodEnd.Format("2006-01-02"))
fmt.Fprintf(w, " Status: %s %s\n", StatusIcon(report.Status), report.Status)
fmt.Fprintf(w, " Score: %.1f%%\n\n", report.Score)
// Summary
fmt.Fprintf(w, " SUMMARY\n")
fmt.Fprintf(w, " %s\n", strings.Repeat("-", 40))
fmt.Fprintf(w, " Controls: %d total, %d compliant, %d non-compliant\n",
report.Summary.TotalControls,
report.Summary.CompliantControls,
report.Summary.NonCompliantControls)
fmt.Fprintf(w, " Compliance: %.1f%%\n", report.Summary.ComplianceRate)
fmt.Fprintf(w, " Open Findings: %d (critical: %d, high: %d)\n",
report.Summary.OpenFindings,
report.Summary.CriticalFindings,
report.Summary.HighFindings)
fmt.Fprintf(w, " Risk Score: %.1f\n\n", report.Summary.RiskScore)
// Categories
fmt.Fprintf(w, " CATEGORIES\n")
fmt.Fprintf(w, " %s\n", strings.Repeat("-", 40))
for _, cat := range report.Categories {
fmt.Fprintf(w, " %s %-25s %.0f%%\n", StatusIcon(cat.Status), cat.Name, cat.Score)
}
fmt.Fprintln(w)
// Findings
if len(report.Findings) > 0 {
fmt.Fprintf(w, " FINDINGS\n")
fmt.Fprintf(w, " %s\n", strings.Repeat("-", 40))
for _, f := range report.Findings {
fmt.Fprintf(w, " %s [%s] %s\n", SeverityIcon(f.Severity), f.Severity, f.Title)
fmt.Fprintf(w, " %s\n", f.Description)
}
fmt.Fprintln(w)
}
fmt.Fprintf(w, "%s\n", strings.Repeat("=", 60))
return nil
}