This commit is contained in:
Dev
2025-09-12 17:01:54 +03:00
commit 815237d804
16 changed files with 2595 additions and 0 deletions

395
pkg/reporter/reporter.go Normal file
View 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>
`