1
0
Fork 0
loadr/lib/metrics.go

218 lines
6.4 KiB
Go
Raw Normal View History

2024-12-04 16:40:52 -06:00
package lib
import (
2024-12-04 17:45:27 -06:00
"encoding/csv"
2024-12-04 16:40:52 -06:00
"fmt"
"math"
"net/http"
"os"
"path/filepath"
2024-12-04 17:45:27 -06:00
"sort"
2024-12-04 16:40:52 -06:00
"strings"
"time"
)
// Global metrics instance to track performance during load testing
2024-12-04 16:40:52 -06:00
var metrics = PerformanceMetrics{
2024-12-04 18:25:51 -06:00
MinLatency: time.Duration(math.MaxInt64),
ResponseCounters: make(map[int]int32),
RequestLatencies: make([]RequestMetric, 0),
2024-12-04 16:40:52 -06:00
}
2024-12-04 17:45:27 -06:00
// UpdateMetrics records metrics for each individual request
// Parameters:
// - duration: time taken to complete the request
// - resp: HTTP response from the request
// - second: elapsed seconds since the start of the test
2024-12-04 16:40:52 -06:00
func UpdateMetrics(duration time.Duration, resp *http.Response, second int) {
2024-12-04 18:25:51 -06:00
metrics.Mu.Lock()
defer metrics.Mu.Unlock()
// Create and store individual request metric
metric := RequestMetric{
Timestamp: time.Now(),
Duration: duration,
StatusCode: resp.StatusCode,
Verb: resp.Request.Method,
}
metrics.RequestLatencies = append(metrics.RequestLatencies, metric)
// Update aggregate metrics
metrics.TotalRequests++
metrics.TotalLatency += duration
// Update max/min latencies
if duration > metrics.MaxLatency {
metrics.MaxLatency = duration
}
if duration < metrics.MinLatency {
metrics.MinLatency = duration
}
// Track successful responses
if resp.StatusCode == http.StatusOK {
metrics.TotalResponses++
metrics.ResponseCounters[second]++
}
2024-12-04 17:45:27 -06:00
}
// calculatePercentile calculates the nth percentile of latencies
// Parameters:
// - latencies: sorted slice of request durations
// - percentile: desired percentile (e.g., 50 for p50, 95 for p95)
2024-12-04 18:25:51 -06:00
//
2024-12-04 17:45:27 -06:00
// Returns: the duration at the specified percentile
func calculatePercentile(latencies []time.Duration, percentile float64) time.Duration {
2024-12-04 18:25:51 -06:00
if len(latencies) == 0 {
return 0
}
index := int(math.Ceil((percentile/100.0)*float64(len(latencies)))) - 1
if index < 0 {
index = 0
}
return latencies[index]
2024-12-04 16:40:52 -06:00
}
2024-12-04 17:45:27 -06:00
// CalculateAndPrintMetrics generates and saves comprehensive test results
// Parameters:
// - startTime: when the load test began
// - requestsPerSecond: target request rate
// - endpoint: URL being tested
// - patterns: request patterns used in the test
2024-12-04 16:40:52 -06:00
func CalculateAndPrintMetrics(startTime time.Time, requestsPerSecond float64, endpoint string, patterns []RequestPattern) {
2024-12-04 18:25:51 -06:00
// Small delay to ensure all metrics are captured
time.Sleep(100 * time.Millisecond)
metrics.Mu.Lock()
defer metrics.Mu.Unlock()
// Prepare latencies for percentile calculations
latencies := make([]time.Duration, len(metrics.RequestLatencies))
for i, metric := range metrics.RequestLatencies {
latencies[i] = metric.Duration
}
sort.Slice(latencies, func(i, j int) bool {
return latencies[i] < latencies[j]
})
// Calculate various percentiles
p50 := calculatePercentile(latencies, 50)
p95 := calculatePercentile(latencies, 95)
p99 := calculatePercentile(latencies, 99)
// Calculate average latency
averageLatency := time.Duration(0)
if metrics.TotalRequests > 0 {
averageLatency = metrics.TotalLatency / time.Duration(metrics.TotalRequests)
}
// Calculate total duration and responses
totalDuration := time.Since(startTime).Seconds()
totalResponses := int32(0)
for _, count := range metrics.ResponseCounters {
totalResponses += count
}
// Reset min latency if unused
if metrics.MinLatency == time.Duration(math.MaxInt64) {
metrics.MinLatency = 0
}
// Build detailed results string
results := fmt.Sprintf("Load Test Report\n=============\n\n")
results += fmt.Sprintf("Endpoint: %s\n", endpoint)
results += fmt.Sprintf("Pattern: ")
// Format request patterns
for i, p := range patterns {
if i > 0 {
results += " → "
}
if p.Percentage > 0 && p.Percentage < 100 {
results += fmt.Sprintf("%.0f%%%s", p.Percentage, strings.ToLower(p.Verb[:1]))
} else {
results += fmt.Sprintf("%d%s", p.Sequence, strings.ToLower(p.Verb[:1]))
}
}
results += "\n\n"
// Add performance metrics
results += fmt.Sprintf("Performance Metrics\n")
results += fmt.Sprintf("-----------------\n")
results += fmt.Sprintf("Total Requests Sent: %d\n", metrics.TotalRequests)
results += fmt.Sprintf("Total Responses Received: %d\n", totalResponses)
results += fmt.Sprintf("Average Latency: %s\n", averageLatency)
results += fmt.Sprintf("Max Latency: %s\n", metrics.MaxLatency)
results += fmt.Sprintf("Min Latency: %s\n", metrics.MinLatency)
results += fmt.Sprintf("Requests/sec (Target): %.2f\n", requestsPerSecond)
results += fmt.Sprintf("Requests/sec (Actual): %.2f\n", float64(metrics.TotalRequests)/totalDuration)
results += fmt.Sprintf("Responses/sec: %.2f\n", float64(totalResponses)/totalDuration)
// Add percentile information
results += fmt.Sprintf("\nLatency Percentiles\n")
results += fmt.Sprintf("-----------------\n")
results += fmt.Sprintf("50th percentile (p50): %s\n", p50)
results += fmt.Sprintf("95th percentile (p95): %s\n", p95)
results += fmt.Sprintf("99th percentile (p99): %s\n", p99)
fmt.Println(results)
saveReport(results)
saveDetailedMetrics()
2024-12-04 17:45:27 -06:00
}
// saveDetailedMetrics writes per-request metrics to a CSV file
// The CSV includes timestamp, duration, status code, and request type for each request
func saveDetailedMetrics() {
2024-12-04 18:25:51 -06:00
resultsDir := ".reports"
os.MkdirAll(resultsDir, os.ModePerm)
csvFile := filepath.Join(resultsDir, fmt.Sprintf("detailed_metrics_%d.csv", time.Now().Unix()))
file, err := os.Create(csvFile)
if err != nil {
fmt.Println("Error creating CSV file:", err)
return
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Write CSV header
writer.Write([]string{
"Timestamp",
"Duration (ms)",
"Status Code",
"Request Type",
})
// Write individual request metrics
for _, metric := range metrics.RequestLatencies {
writer.Write([]string{
metric.Timestamp.Format(time.RFC3339),
fmt.Sprintf("%.2f", float64(metric.Duration.Milliseconds())),
fmt.Sprintf("%d", metric.StatusCode),
metric.Verb,
})
}
fmt.Println("Detailed metrics saved:", csvFile)
2024-12-04 16:40:52 -06:00
}
2024-12-04 17:45:27 -06:00
// saveReport writes the summary report to a text file
// Parameters:
2024-12-04 17:45:27 -06:00
// - results: formatted string containing the complete test results
2024-12-04 16:40:52 -06:00
func saveReport(results string) {
2024-12-04 18:25:51 -06:00
resultsDir := ".reports"
os.MkdirAll(resultsDir, os.ModePerm)
2024-12-04 16:40:52 -06:00
2024-12-04 18:25:51 -06:00
resultsFile := filepath.Join(resultsDir, fmt.Sprintf("summary_%d.txt", time.Now().Unix()))
2024-12-04 18:25:51 -06:00
if err := os.WriteFile(resultsFile, []byte(results), 0644); err != nil {
fmt.Println("Error saving report:", err)
return
}
2024-12-04 16:40:52 -06:00
2024-12-04 18:25:51 -06:00
fmt.Println("Summary report saved:", resultsFile)
2024-12-04 16:40:52 -06:00
}