From d2c50825fdb72fcc2e996d1cbb43655917595f1d Mon Sep 17 00:00:00 2001 From: atridadl Date: Tue, 16 Jan 2024 09:27:38 -0700 Subject: [PATCH] Updates to clean up the library! <3 --- README.md | 2 +- lib/metrics.go | 99 ++++++++++++++++++++++++++ lib/requests.go | 103 +++++++++++++++++++++++++++ main.go | 186 +++++------------------------------------------- 4 files changed, 219 insertions(+), 171 deletions(-) create mode 100644 lib/metrics.go create mode 100644 lib/requests.go diff --git a/README.md b/README.md index 9aee0bd..8f1c37e 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ A lightweight REST load testing tool with robust support for different verbs, to ## Reports -Reports are logged at the end of a test run. They are also saved in a directory called `.reports`. All reports are saved as text files with `YYYYMMdd-HHmmss` time format names. +Reports are logged at the end of a test run. They are also saved in a directory called `.reports`. All reports are saved as text files with names corresponding to the unix timestamp of when they were completed. diff --git a/lib/metrics.go b/lib/metrics.go new file mode 100644 index 0000000..e5ab394 --- /dev/null +++ b/lib/metrics.go @@ -0,0 +1,99 @@ +package lib + +import ( + "fmt" + "math" + "net/http" + "os" + "path/filepath" + "sync" + "time" +) + +// PerformanceMetrics holds the metrics for performance evaluation. +type PerformanceMetrics struct { + mu sync.Mutex // Protects the metrics + + totalRequests int32 + totalResponses int32 + totalLatency time.Duration + maxLatency time.Duration + minLatency time.Duration + responseCounters map[int]int32 +} + +// Initialize the metrics with default values. +var metrics = PerformanceMetrics{ + minLatency: time.Duration(math.MaxInt64), + responseCounters: make(map[int]int32), +} + +// updateMetrics updates the performance metrics. +func UpdateMetrics(duration time.Duration, resp *http.Response, second int) { + metrics.mu.Lock() + defer metrics.mu.Unlock() + + metrics.totalRequests++ + metrics.totalLatency += duration + if duration > metrics.maxLatency { + metrics.maxLatency = duration + } + if duration < metrics.minLatency { + metrics.minLatency = duration + } + if resp.StatusCode == http.StatusOK { + metrics.totalResponses++ + metrics.responseCounters[second]++ + } +} + +// calculateAndPrintMetrics calculates and prints the performance metrics. +func CalculateAndPrintMetrics(startTime time.Time, requestsPerSecond float64, endpoint string, verb string) { + averageLatency := time.Duration(0) + if metrics.totalRequests > 0 { + averageLatency = metrics.totalLatency / time.Duration(metrics.totalRequests) + } + + totalDuration := time.Since(startTime).Seconds() + totalResponses := int32(0) + for _, count := range metrics.responseCounters { + totalResponses += count + } + + // Format the results + results := fmt.Sprintf("Endpoint: %s\n", endpoint) + results += fmt.Sprintf("HTTP Verb: %s\n", verb) + results += fmt.Sprintln("--------------------") + results += fmt.Sprintln("Performance Metrics:") + 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 Per Second (Sent): %.2f\n", float64(requestsPerSecond)) + results += fmt.Sprintf("Responses Per Second (Received): %.2f\n", float64(totalResponses)/totalDuration) + + // Print the results to the console + fmt.Println(results) + + // Save the results to a file + resultsDir := ".reports" + os.MkdirAll(resultsDir, os.ModePerm) // Ensure the directory exists + + // Use the current epoch timestamp as the filename + resultsFile := filepath.Join(resultsDir, fmt.Sprintf("%d.txt", time.Now().Unix())) + f, err := os.Create(resultsFile) + if err != nil { + fmt.Println("Error creating file: ", err) + return + } + defer f.Close() + + _, err = f.WriteString(results) + if err != nil { + fmt.Println("Error writing to file: ", err) + return + } + + fmt.Println("Results saved to: ", resultsFile) +} diff --git a/lib/requests.go b/lib/requests.go new file mode 100644 index 0000000..7271fef --- /dev/null +++ b/lib/requests.go @@ -0,0 +1,103 @@ +package lib + +import ( + "bytes" + "fmt" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" +) + +// Global HTTP client used for making requests. +var client = &http.Client{} + +type RequestError struct { + Verb string + URL string + Err error +} + +func (e *RequestError) Error() string { + return fmt.Sprintf("error making %s request to %s: %v", e.Verb, e.URL, e.Err) +} + +// makeRequest sends an HTTP request and updates performance metrics. +func makeRequest(verb, url, token string, jsonData []byte, second int) error { + startTime := time.Now() + + // Create a new request with the provided verb, URL, and JSON data if provided. + var req *http.Request + var err error + if jsonData != nil { + req, err = http.NewRequest(verb, url, bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + } else { + req, err = http.NewRequest(verb, url, nil) + } + if err != nil { + return &RequestError{Verb: verb, URL: url, Err: err} + } + + // Add the bearer token to the request's Authorization header if provided. + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + // Send the request. + resp, err := client.Do(req) + if err != nil { + return &RequestError{Verb: verb, URL: url, Err: err} + } + defer resp.Body.Close() + + // Calculate the duration of the request. + duration := time.Since(startTime) + + // Update the performance metrics in a separate goroutine. + go UpdateMetrics(duration, resp, second) + + return nil +} + +// sendRequests sends requests at the specified rate. +func SendRequests(url, bearerToken, requestType string, jsonData []byte, maxRequests int, requestsPerSecond float64) { + // Calculate the rate limit based on the requests per second. + rateLimit := time.Second / time.Duration(requestsPerSecond) + ticker := time.NewTicker(rateLimit) + defer ticker.Stop() + + // Initialize the request count. + var requestCount int32 = 0 + + // Wait for all goroutines to finish. + var wg sync.WaitGroup + + // Log beginning of requests + fmt.Println("Starting Loadr Requests...") + + // Start sending requests at the specified rate. + startTime := time.Now() + for range ticker.C { + second := int(time.Since(startTime).Seconds()) + if int(requestCount) >= maxRequests { + break + } + wg.Add(1) + go func(u, t, verb string, data []byte, sec int) { + defer wg.Done() + err := makeRequest(verb, u, t, data, sec) + if err != nil { + fmt.Println(err) + return + } + + atomic.AddInt32(&requestCount, 1) + }(url, bearerToken, strings.ToUpper(requestType), jsonData, second) + } + + wg.Wait() // Wait for all requests to finish. + + CalculateAndPrintMetrics(startTime, requestsPerSecond, url, requestType) +} diff --git a/main.go b/main.go index 7447b94..18b137e 100644 --- a/main.go +++ b/main.go @@ -1,97 +1,25 @@ package main import ( - "bytes" "flag" "fmt" - "io" - "math" - "net/http" + "loadr/lib" "os" - "path/filepath" - "strings" - "sync" - "sync/atomic" - "time" ) -// Global HTTP client used for making requests. -var client = &http.Client{} +// parseCommandLine parses the command-line flags and returns the parsed values. +func parseCommandLine() (float64, int, string, string, string, string) { + requestsPerSecond := flag.Float64("rate", 10, "Number of requests per second") + maxRequests := flag.Int("max", 50, "Maximum number of requests to send (0 for unlimited)") + url := flag.String("url", "https://example.com", "The URL to make requests to") + requestType := flag.String("type", "GET", "Type of HTTP request (GET, POST, PUT, DELETE, etc.)") + jsonFilePath := flag.String("json", "", "Path to the JSON file with request data") + bearerToken := flag.String("token", "", "Bearer token for authorization") -// PerformanceMetrics holds the metrics for performance evaluation. -type PerformanceMetrics struct { - mu sync.Mutex // Protects the metrics + // Parse the command-line flags. + flag.Parse() - totalRequests int32 - totalResponses int32 - totalLatency time.Duration - maxLatency time.Duration - minLatency time.Duration - responseCounters map[int]int32 -} - -// Initialize the metrics with default values. -var metrics = PerformanceMetrics{ - minLatency: time.Duration(math.MaxInt64), - responseCounters: make(map[int]int32), -} - -// makeRequest sends an HTTP request and updates performance metrics. -func makeRequest(verb, url, token string, jsonData []byte, second int) { - startTime := time.Now() - - // Create a new request with the provided verb, URL, and JSON data if provided. - var req *http.Request - var err error - if jsonData != nil { - req, err = http.NewRequest(verb, url, bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - } else { - req, err = http.NewRequest(verb, url, nil) - } - if err != nil { - fmt.Println("Error creating request:", err) - return - } - - // Add the bearer token to the request's Authorization header if provided. - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - - // Send the request. - resp, err := client.Do(req) - if err != nil { - fmt.Println("Error making request:", err) - return - } - defer resp.Body.Close() - - // Calculate the duration of the request. - duration := time.Since(startTime) - - // Update the performance metrics. - metrics.mu.Lock() - metrics.totalRequests++ - metrics.totalLatency += duration - if duration > metrics.maxLatency { - metrics.maxLatency = duration - } - if duration < metrics.minLatency { - metrics.minLatency = duration - } - if resp.StatusCode == http.StatusOK { - metrics.totalResponses++ - metrics.responseCounters[second]++ - } - metrics.mu.Unlock() - - // Read the response body to determine its size (not shown in the output). - _, err = io.ReadAll(resp.Body) - if err != nil { - fmt.Println("Error reading response body:", err) - return - } + return *requestsPerSecond, *maxRequests, *url, *requestType, *jsonFilePath, *bearerToken } // readJSONFile reads the contents of the JSON file at the given path and returns the bytes. @@ -103,103 +31,21 @@ func readJSONFile(filePath string) ([]byte, error) { } func main() { - // Define command-line flags for configuring the load test. - requestsPerSecond := flag.Float64("rate", 10, "Number of requests per second") - maxRequests := flag.Int("max", 50, "Maximum number of requests to send (0 for unlimited)") - url := flag.String("url", "https://example.com", "The URL to make requests to") - requestType := flag.String("type", "GET", "Type of HTTP request (GET, POST, PUT, DELETE, etc.)") - jsonFilePath := flag.String("json", "", "Path to the JSON file with request data") - bearerToken := flag.String("token", "", "Bearer token for authorization") - // Parse the command-line flags. - flag.Parse() + requestsPerSecond, maxRequests, url, requestType, jsonFilePath, bearerToken := parseCommandLine() // Ensure maxRequests is greater than 0. - if *maxRequests <= 0 { + if maxRequests <= 0 { fmt.Println("Error: max must be an integer greater than 0") return } // Read the JSON file if the path is provided. - jsonData, err := readJSONFile(*jsonFilePath) + jsonData, err := readJSONFile(jsonFilePath) if err != nil { fmt.Println("Error reading JSON file:", err) return } - // Calculate the rate limit based on the requests per second. - rateLimit := time.Second / time.Duration(*requestsPerSecond) - ticker := time.NewTicker(rateLimit) - defer ticker.Stop() - - // Initialize the request count. - var requestCount int32 = 0 - - // Wait for all goroutines to finish. - var wg sync.WaitGroup - - // Log beginning of requests - fmt.Println("Starting Loadr Requests...") - - // Start sending requests at the specified rate. - startTime := time.Now() - for range ticker.C { - second := int(time.Since(startTime).Seconds()) - if int(requestCount) >= *maxRequests { - break - } - wg.Add(1) - go func(u, t, verb string, data []byte, sec int) { - defer wg.Done() - makeRequest(verb, u, t, data, sec) - atomic.AddInt32(&requestCount, 1) - }(*url, *bearerToken, strings.ToUpper(*requestType), jsonData, second) - } - - wg.Wait() // Wait for all requests to finish. - - // Calculate and print performance metrics. - averageLatency := time.Duration(0) - if metrics.totalRequests > 0 { - averageLatency = metrics.totalLatency / time.Duration(metrics.totalRequests) - } - - totalDuration := time.Since(startTime).Seconds() - totalResponses := int32(0) - for _, count := range metrics.responseCounters { - totalResponses += count - } - - // Format the results - results := fmt.Sprintln("Performance Metrics:") - 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 Per Second (Sent): %.2f\n", float64(*requestsPerSecond)) - results += fmt.Sprintf("Responses Per Second (Received): %.2f\n", float64(totalResponses)/totalDuration) - - // Print the results to the console - fmt.Print(results) - - // Ensure the .reports directory exists - reportsDir := ".reports" - if _, err := os.Stat(reportsDir); os.IsNotExist(err) { - err := os.Mkdir(reportsDir, 0755) - if err != nil { - fmt.Println("Error creating reports directory:", err) - return - } - } - - // Save the results to a file in the .reports directory - timestamp := time.Now().Format("20060102-150405") // YYYYMMdd-HHmmss format - fileName := fmt.Sprintf("%s.txt", timestamp) - filePath := filepath.Join(reportsDir, fileName) - if err := os.WriteFile(filePath, []byte(results), 0644); err != nil { - fmt.Println("Error writing results to file:", err) - return - } - + lib.SendRequests(url, bearerToken, requestType, jsonData, maxRequests, requestsPerSecond) }