1
This commit is contained in:
395
pkg/reporter/reporter.go
Normal file
395
pkg/reporter/reporter.go
Normal file
@@ -0,0 +1,395 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/stroke/pkg/metrics"
|
||||
)
|
||||
|
||||
// Reporter interface for different output formats
|
||||
type Reporter interface {
|
||||
Generate(results *metrics.Results, outputDir string) error
|
||||
}
|
||||
|
||||
// ConsoleReporter outputs results to the console
|
||||
type ConsoleReporter struct{}
|
||||
|
||||
// NewConsoleReporter creates a new console reporter
|
||||
func NewConsoleReporter() *ConsoleReporter {
|
||||
return &ConsoleReporter{}
|
||||
}
|
||||
|
||||
// Generate outputs results to console
|
||||
func (cr *ConsoleReporter) Generate(results *metrics.Results, outputDir string) error {
|
||||
fmt.Printf("\n📊 Stroke Test Results\n")
|
||||
fmt.Print(strings.Repeat("=", 50) + "\n")
|
||||
|
||||
fmt.Printf("Duration: %v\n", results.TestDuration)
|
||||
fmt.Printf("Total Requests: %d\n", results.TotalRequests)
|
||||
fmt.Printf("Successful: %d\n", results.SuccessRequests)
|
||||
fmt.Printf("Failed: %d\n", results.FailedRequests)
|
||||
fmt.Printf("RPS: %.2f\n", results.RequestsPerSecond)
|
||||
|
||||
fmt.Printf("\nResponse Times:\n")
|
||||
fmt.Printf(" Min: %v\n", results.MinResponseTime)
|
||||
fmt.Printf(" Max: %v\n", results.MaxResponseTime)
|
||||
fmt.Printf(" Avg: %v\n", results.AvgResponseTime)
|
||||
fmt.Printf(" p50: %v\n", results.P50)
|
||||
fmt.Printf(" p90: %v\n", results.P90)
|
||||
fmt.Printf(" p95: %v\n", results.P95)
|
||||
fmt.Printf(" p99: %v\n", results.P99)
|
||||
|
||||
if len(results.StatusCodes) > 0 {
|
||||
fmt.Printf("\nStatus Codes:\n")
|
||||
for code, count := range results.StatusCodes {
|
||||
fmt.Printf(" %d: %d\n", code, count)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONReporter outputs results as JSON
|
||||
type JSONReporter struct{}
|
||||
|
||||
// NewJSONReporter creates a new JSON reporter
|
||||
func NewJSONReporter() *JSONReporter {
|
||||
return &JSONReporter{}
|
||||
}
|
||||
|
||||
// Generate outputs results as JSON file
|
||||
func (jr *JSONReporter) Generate(results *metrics.Results, outputDir string) error {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
filename := filepath.Join(outputDir, fmt.Sprintf("stroke_results_%s.json", timestamp))
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create JSON file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(results); err != nil {
|
||||
return fmt.Errorf("failed to encode JSON: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("📄 JSON report saved to: %s\n", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTMLReporter generates beautiful HTML reports
|
||||
type HTMLReporter struct{}
|
||||
|
||||
// NewHTMLReporter creates a new HTML reporter
|
||||
func NewHTMLReporter() *HTMLReporter {
|
||||
return &HTMLReporter{}
|
||||
}
|
||||
|
||||
// Generate creates an HTML report
|
||||
func (hr *HTMLReporter) Generate(results *metrics.Results, outputDir string) error {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
filename := filepath.Join(outputDir, fmt.Sprintf("stroke_report_%s.html", timestamp))
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create HTML file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tmpl := template.Must(template.New("report").Parse(htmlTemplate))
|
||||
|
||||
data := struct {
|
||||
Results *metrics.Results
|
||||
Timestamp string
|
||||
Title string
|
||||
}{
|
||||
Results: results,
|
||||
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Title: "Stroke Stress Test Report",
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(file, data); err != nil {
|
||||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("📊 HTML report saved to: %s\n", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CSVReporter outputs results as CSV
|
||||
type CSVReporter struct{}
|
||||
|
||||
// NewCSVReporter creates a new CSV reporter
|
||||
func NewCSVReporter() *CSVReporter {
|
||||
return &CSVReporter{}
|
||||
}
|
||||
|
||||
// Generate outputs results as CSV file
|
||||
func (csvr *CSVReporter) Generate(results *metrics.Results, outputDir string) error {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
filename := filepath.Join(outputDir, fmt.Sprintf("stroke_results_%s.csv", timestamp))
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create CSV file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write CSV header
|
||||
fmt.Fprintf(file, "Metric,Value\n")
|
||||
fmt.Fprintf(file, "Total Requests,%d\n", results.TotalRequests)
|
||||
fmt.Fprintf(file, "Success Requests,%d\n", results.SuccessRequests)
|
||||
fmt.Fprintf(file, "Failed Requests,%d\n", results.FailedRequests)
|
||||
fmt.Fprintf(file, "Test Duration,%v\n", results.TestDuration)
|
||||
fmt.Fprintf(file, "Requests Per Second,%.2f\n", results.RequestsPerSecond)
|
||||
fmt.Fprintf(file, "Min Response Time,%v\n", results.MinResponseTime)
|
||||
fmt.Fprintf(file, "Max Response Time,%v\n", results.MaxResponseTime)
|
||||
fmt.Fprintf(file, "Avg Response Time,%v\n", results.AvgResponseTime)
|
||||
fmt.Fprintf(file, "P50 Response Time,%v\n", results.P50)
|
||||
fmt.Fprintf(file, "P90 Response Time,%v\n", results.P90)
|
||||
fmt.Fprintf(file, "P95 Response Time,%v\n", results.P95)
|
||||
fmt.Fprintf(file, "P99 Response Time,%v\n", results.P99)
|
||||
|
||||
fmt.Printf("📈 CSV report saved to: %s\n", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiReporter combines multiple reporters
|
||||
type MultiReporter struct {
|
||||
reporters []Reporter
|
||||
}
|
||||
|
||||
// NewMultiReporter creates a reporter that outputs to multiple formats
|
||||
func NewMultiReporter(formats []string) *MultiReporter {
|
||||
var reporters []Reporter
|
||||
|
||||
for _, format := range formats {
|
||||
switch strings.ToLower(format) {
|
||||
case "console":
|
||||
reporters = append(reporters, NewConsoleReporter())
|
||||
case "json":
|
||||
reporters = append(reporters, NewJSONReporter())
|
||||
case "html":
|
||||
reporters = append(reporters, NewHTMLReporter())
|
||||
case "csv":
|
||||
reporters = append(reporters, NewCSVReporter())
|
||||
}
|
||||
}
|
||||
|
||||
return &MultiReporter{reporters: reporters}
|
||||
}
|
||||
|
||||
// Generate runs all configured reporters
|
||||
func (mr *MultiReporter) Generate(results *metrics.Results, outputDir string) error {
|
||||
for _, reporter := range mr.reporters {
|
||||
if err := reporter.Generate(results, outputDir); err != nil {
|
||||
return fmt.Errorf("reporter failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTML template for the report
|
||||
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>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.metric-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-left: 4px solid #007bff;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.metric-title {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
margin-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 1.8em;
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
}
|
||||
.status-codes {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.status-codes h3 {
|
||||
margin-top: 0;
|
||||
color: #495057;
|
||||
}
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
.footer {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.emoji {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.success { border-left-color: #28a745; }
|
||||
.warning { border-left-color: #ffc107; }
|
||||
.danger { border-left-color: #dc3545; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><span class="emoji">🚀</span> {{.Title}} <span class="emoji">🚀</span></h1>
|
||||
<p>Generated on {{.Timestamp}}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card success">
|
||||
<div class="metric-title">Total Requests</div>
|
||||
<div class="metric-value">{{.Results.TotalRequests}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card success">
|
||||
<div class="metric-title">Successful Requests</div>
|
||||
<div class="metric-value">{{.Results.SuccessRequests}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card {{if gt .Results.FailedRequests 0}}danger{{else}}success{{end}}">
|
||||
<div class="metric-title">Failed Requests</div>
|
||||
<div class="metric-value">{{.Results.FailedRequests}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Test Duration</div>
|
||||
<div class="metric-value">{{.Results.TestDuration}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Requests Per Second</div>
|
||||
<div class="metric-value">{{printf "%.2f" .Results.RequestsPerSecond}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Average Response Time</div>
|
||||
<div class="metric-value">{{.Results.AvgResponseTime}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">P50 Response Time</div>
|
||||
<div class="metric-value">{{.Results.P50}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">P90 Response Time</div>
|
||||
<div class="metric-value">{{.Results.P90}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">P95 Response Time</div>
|
||||
<div class="metric-value">{{.Results.P95}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">P99 Response Time</div>
|
||||
<div class="metric-value">{{.Results.P99}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Min Response Time</div>
|
||||
<div class="metric-value">{{.Results.MinResponseTime}}</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Max Response Time</div>
|
||||
<div class="metric-value">{{.Results.MaxResponseTime}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Results.StatusCodes}}
|
||||
<div class="status-codes">
|
||||
<h3><span class="emoji">📈</span> Status Code Distribution</h3>
|
||||
{{range $code, $count := .Results.StatusCodes}}
|
||||
<div class="status-item">
|
||||
<span>HTTP {{$code}}</span>
|
||||
<span><strong>{{$count}} requests</strong></span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><span class="emoji">💪</span> Powered by Stroke - Because Your Server Needs Some Exercise <span class="emoji">💪</span></p>
|
||||
<p>Made with ❤️ and a healthy dose of sarcasm by @iwasforcedtobehere</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
Reference in New Issue
Block a user