Files
stroke/pkg/metrics/metrics.go
2025-09-12 17:01:54 +03:00

190 lines
4.7 KiB
Go

package metrics
import (
"sort"
"sync"
"time"
)
// Collector handles metrics collection during stress testing
type Collector struct {
mu sync.RWMutex
responseTimes []time.Duration
statusCodes map[int]int64
errors []error
totalRequests int64
successRequests int64
failedRequests int64
startTime time.Time
}
// Results contains aggregated test results
type Results struct {
TotalRequests int64
SuccessRequests int64
FailedRequests int64
MinResponseTime time.Duration
MaxResponseTime time.Duration
AvgResponseTime time.Duration
P50 time.Duration
P90 time.Duration
P95 time.Duration
P99 time.Duration
StatusCodes map[int]int64
Errors []error
TestDuration time.Duration
RequestsPerSecond float64
}
// NewCollector creates a new metrics collector
func NewCollector() *Collector {
return &Collector{
responseTimes: make([]time.Duration, 0),
statusCodes: make(map[int]int64),
errors: make([]error, 0),
startTime: time.Now(),
}
}
// RecordRequest records a single request's metrics
func (c *Collector) RecordRequest(responseTime time.Duration, statusCode int, err error) {
c.mu.Lock()
defer c.mu.Unlock()
c.totalRequests++
c.responseTimes = append(c.responseTimes, responseTime)
if err != nil {
c.failedRequests++
c.errors = append(c.errors, err)
} else {
c.successRequests++
c.statusCodes[statusCode]++
}
}
// GetResults calculates and returns aggregated results
func (c *Collector) GetResults() *Results {
c.mu.RLock()
defer c.mu.RUnlock()
testDuration := time.Since(c.startTime)
rps := float64(c.totalRequests) / testDuration.Seconds()
if len(c.responseTimes) == 0 {
return &Results{
TotalRequests: c.totalRequests,
SuccessRequests: c.successRequests,
FailedRequests: c.failedRequests,
StatusCodes: copyMap(c.statusCodes),
Errors: copyErrors(c.errors),
TestDuration: testDuration,
RequestsPerSecond: rps,
}
}
// Filter out zero response times (from errors) and sort for percentile calculations
var validTimes []time.Duration
for _, t := range c.responseTimes {
if t > 0 {
validTimes = append(validTimes, t)
}
}
if len(validTimes) == 0 {
return &Results{
TotalRequests: c.totalRequests,
SuccessRequests: c.successRequests,
FailedRequests: c.failedRequests,
StatusCodes: copyMap(c.statusCodes),
Errors: copyErrors(c.errors),
TestDuration: testDuration,
RequestsPerSecond: rps,
}
}
sort.Slice(validTimes, func(i, j int) bool {
return validTimes[i] < validTimes[j]
})
// Calculate statistics
minTime := validTimes[0]
maxTime := validTimes[len(validTimes)-1]
avgTime := calculateAverage(validTimes)
return &Results{
TotalRequests: c.totalRequests,
SuccessRequests: c.successRequests,
FailedRequests: c.failedRequests,
MinResponseTime: minTime,
MaxResponseTime: maxTime,
AvgResponseTime: avgTime,
P50: calculatePercentile(validTimes, 50),
P90: calculatePercentile(validTimes, 90),
P95: calculatePercentile(validTimes, 95),
P99: calculatePercentile(validTimes, 99),
StatusCodes: copyMap(c.statusCodes),
Errors: copyErrors(c.errors),
TestDuration: testDuration,
RequestsPerSecond: rps,
}
}
// Reset clears all collected metrics
func (c *Collector) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.responseTimes = c.responseTimes[:0]
c.statusCodes = make(map[int]int64)
c.errors = c.errors[:0]
c.totalRequests = 0
c.successRequests = 0
c.failedRequests = 0
c.startTime = time.Now()
}
// calculatePercentile calculates the nth percentile of sorted response times
func calculatePercentile(sortedTimes []time.Duration, percentile int) time.Duration {
if len(sortedTimes) == 0 {
return 0
}
index := int(float64(len(sortedTimes)-1) * float64(percentile) / 100.0)
if index >= len(sortedTimes) {
index = len(sortedTimes) - 1
}
return sortedTimes[index]
}
// calculateAverage calculates the average response time
func calculateAverage(times []time.Duration) time.Duration {
if len(times) == 0 {
return 0
}
var total time.Duration
for _, t := range times {
total += t
}
return total / time.Duration(len(times))
}
// copyMap creates a copy of the status codes map
func copyMap(original map[int]int64) map[int]int64 {
copy := make(map[int]int64)
for k, v := range original {
copy[k] = v
}
return copy
}
// copyErrors creates a copy of the errors slice
func copyErrors(original []error) []error {
copy := make([]error, len(original))
copy = append(copy, original...)
return copy
}