mirror of
https://github.com/americanexpress/baton.git
synced 2025-07-06 12:22:29 +00:00

This works best when a request count is provided, if not the statistics are based on the first `n` requests where is `n` is a predefined constant
242 lines
7.3 KiB
Go
242 lines
7.3 KiB
Go
/*
|
|
* Copyright 2018 American Express
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
* or implied. See the License for the specific language governing
|
|
* permissions and limitations under the License.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"flag"
|
|
"github.com/valyala/fasthttp"
|
|
"io/ioutil"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
body = flag.String("b", "", "Body (use instead of -f)")
|
|
concurrency = flag.Int("c", 1, "Number of concurrent requests")
|
|
dataFilePath = flag.String("f", "", "File path to file to be used as the body (use instead of -b)")
|
|
duration = flag.Int("t", 0, "Duration of testing in seconds (use instead of -r)")
|
|
ignoreTLS = flag.Bool("i", false, "Ignore TLS/SSL certificate validation ")
|
|
method = flag.String("m", "GET", "HTTP Method (GET,POST,PUT,DELETE)")
|
|
numberOfRequests = flag.Int("r", 1, "Number of requests (use instead of -t)")
|
|
requestsFromFile = flag.String("z", "", "Read requests from a file")
|
|
suppressOutput = flag.Bool("o", false, "Suppress output, no results will be printed to stdout")
|
|
url = flag.String("u", "", "URL to run against")
|
|
wait = flag.Int("w", 0, "Number of seconds to wait before running test")
|
|
)
|
|
|
|
// Baton implements the load tester
|
|
type Baton struct {
|
|
configuration Configuration
|
|
result Result
|
|
}
|
|
|
|
type preLoadedRequest struct {
|
|
method string // The HTTP method used to send the request
|
|
url string // The URL to send the request at
|
|
body string // The body of the request (if appropriate method is selected)
|
|
headers [][]string // Array of two-element key/value pairs of header and value
|
|
}
|
|
|
|
type runConfiguration struct {
|
|
preLoadedRequestsMode bool
|
|
timedMode bool
|
|
preLoadedRequests []preLoadedRequest
|
|
client *fasthttp.Client
|
|
requests chan bool
|
|
results chan HTTPResult
|
|
done chan bool
|
|
body string
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
configuration := Configuration{
|
|
*body,
|
|
*concurrency,
|
|
*dataFilePath,
|
|
*duration,
|
|
*ignoreTLS,
|
|
*method,
|
|
*numberOfRequests,
|
|
*requestsFromFile,
|
|
*suppressOutput,
|
|
*url,
|
|
*wait,
|
|
}
|
|
|
|
baton := &Baton{configuration: configuration, result: *newResult()}
|
|
|
|
baton.run()
|
|
baton.result.printResults()
|
|
}
|
|
|
|
func (baton *Baton) run() {
|
|
|
|
configureLogging(baton.configuration.suppressOutput)
|
|
|
|
err := baton.configuration.validate()
|
|
if err != nil {
|
|
log.Fatalf("Invalid configuration: %v", err)
|
|
}
|
|
|
|
preparedRunConfiguration, err := prepareRun(baton.configuration)
|
|
if err != nil {
|
|
log.Fatalf("Error during run preparation: %v", err)
|
|
}
|
|
|
|
if baton.configuration.wait > 0 {
|
|
time.Sleep(time.Duration(baton.configuration.wait) * time.Second)
|
|
}
|
|
|
|
log.Println("Sending the requests to the server...")
|
|
|
|
// Start the timer and kick off the workers
|
|
start := time.Now()
|
|
for w := 1; w <= baton.configuration.concurrency; w++ {
|
|
var worker workable
|
|
if preparedRunConfiguration.timedMode {
|
|
worker = newTimedWorker(preparedRunConfiguration.requests, preparedRunConfiguration.results, preparedRunConfiguration.done, float64(baton.configuration.duration))
|
|
} else {
|
|
worker = newCountWorker(preparedRunConfiguration.requests, preparedRunConfiguration.results, preparedRunConfiguration.done)
|
|
}
|
|
worker.setCustomClient(preparedRunConfiguration.client)
|
|
if preparedRunConfiguration.preLoadedRequestsMode {
|
|
go worker.sendRequests(preparedRunConfiguration.preLoadedRequests)
|
|
} else {
|
|
request := preLoadedRequest{baton.configuration.method, baton.configuration.url, preparedRunConfiguration.body, [][]string{}}
|
|
go worker.sendRequest(request)
|
|
}
|
|
}
|
|
|
|
// Wait for all the workers to finish and then stop the timer
|
|
for a := 1; a <= baton.configuration.concurrency; a++ {
|
|
<-preparedRunConfiguration.done
|
|
}
|
|
baton.result.timeTaken = time.Since(start)
|
|
|
|
log.Println("Finished sending the requests")
|
|
log.Println("Processing the results...")
|
|
|
|
processResults(baton, preparedRunConfiguration)
|
|
}
|
|
|
|
func processResults(baton *Baton, preparedRunConfiguration runConfiguration) {
|
|
timeSum := int64(0)
|
|
requestCount := 0
|
|
for a := 1; a <= baton.configuration.concurrency; a++ {
|
|
result := <-preparedRunConfiguration.results
|
|
baton.result.httpResult.connectionErrorCount += result.connectionErrorCount
|
|
baton.result.httpResult.status1xxCount += result.status1xxCount
|
|
baton.result.httpResult.status2xxCount += result.status2xxCount
|
|
baton.result.httpResult.status3xxCount += result.status3xxCount
|
|
baton.result.httpResult.status4xxCount += result.status4xxCount
|
|
baton.result.httpResult.status5xxCount += result.status5xxCount
|
|
if result.minTime < baton.result.httpResult.minTime {
|
|
baton.result.minTime = result.minTime
|
|
}
|
|
if result.maxTime > baton.result.httpResult.maxTime {
|
|
baton.result.maxTime = result.maxTime
|
|
}
|
|
timeSum += result.timeSum
|
|
requestCount += result.totalSuccess
|
|
}
|
|
baton.result.averageTime = float32(timeSum) / float32(requestCount)
|
|
baton.result.totalRequests = baton.result.httpResult.total()
|
|
baton.result.requestsPerSecond = int(float64(baton.result.totalRequests)/baton.result.timeTaken.Seconds() + 0.5)
|
|
}
|
|
|
|
func configureLogging(suppressOutput bool) {
|
|
|
|
logWriter := &logWriter{true}
|
|
|
|
if suppressOutput {
|
|
logWriter.Disable()
|
|
}
|
|
|
|
log.SetFlags(0)
|
|
log.SetOutput(logWriter)
|
|
}
|
|
|
|
func prepareRun(configuration Configuration) (runConfiguration, error) {
|
|
|
|
preLoadedRequestsMode := false
|
|
timedMode := false
|
|
|
|
var preLoadedRequests []preLoadedRequest
|
|
|
|
if configuration.requestsFromFile != "" {
|
|
var err error
|
|
preLoadedRequests, err = preLoadRequestsFromFile(configuration.requestsFromFile)
|
|
preLoadedRequestsMode = true
|
|
if err != nil {
|
|
return runConfiguration{}, errors.New("failed to parse requests from file: " + configuration.requestsFromFile)
|
|
}
|
|
}
|
|
|
|
if configuration.duration != 0 {
|
|
timedMode = true
|
|
}
|
|
|
|
client := &fasthttp.Client{}
|
|
if configuration.ignoreTLS {
|
|
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
|
client = &fasthttp.Client{TLSConfig: tlsConfig}
|
|
}
|
|
|
|
body := configuration.body
|
|
if configuration.dataFilePath != "" {
|
|
data, err := ioutil.ReadFile(configuration.dataFilePath)
|
|
if err != nil {
|
|
return runConfiguration{}, err
|
|
}
|
|
body = string(data)
|
|
}
|
|
|
|
if preLoadedRequestsMode {
|
|
log.Printf("Configuring to send requests from file. (Read %d requests)\n", len(preLoadedRequests))
|
|
} else {
|
|
log.Printf("Configuring to send %s requests to: %s\n", configuration.method, configuration.url)
|
|
}
|
|
|
|
requests := make(chan bool, configuration.numberOfRequests)
|
|
results := make(chan HTTPResult, configuration.concurrency)
|
|
done := make(chan bool, configuration.concurrency)
|
|
|
|
log.Println("Generating the requests...")
|
|
for r := 1; r <= configuration.numberOfRequests; r++ {
|
|
requests <- true
|
|
}
|
|
close(requests)
|
|
log.Println("Finished generating the requests")
|
|
|
|
preparedRunConfiguration := runConfiguration{
|
|
preLoadedRequestsMode,
|
|
timedMode,
|
|
preLoadedRequests,
|
|
client,
|
|
requests,
|
|
results,
|
|
done,
|
|
body,
|
|
}
|
|
|
|
return preparedRunConfiguration, nil
|
|
}
|