feat: adaptive processing of request body format and floating-point decimal point

This commit is contained in:
zhuyasen
2025-09-06 18:51:51 +08:00
parent af78568807
commit c1c203f84d
7 changed files with 158 additions and 45 deletions

View File

@@ -1,6 +1,7 @@
package common
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
@@ -9,6 +10,99 @@ import (
"time"
)
const (
BodyTypeJSON = "application/json"
BodyTypeForm = "application/x-www-form-urlencoded"
BodyTypeText = "text/plain"
)
// nolint
func ParseHTTPParams(method string, headers []string, body string, bodyFile string) ([]byte, map[string]string, error) {
var bodyType string
var bodyBytes []byte
headerMap := make(map[string]string)
for _, h := range headers {
kvs := strings.SplitN(h, ":", 2)
if len(kvs) == 2 {
key := trimString(kvs[0])
value := trimString(kvs[1])
headerMap[key] = value
}
}
if strings.ToUpper(method) == "GET" {
return bodyBytes, headerMap, nil
}
if body != "" {
body = strings.Trim(body, "'")
bodyBytes = []byte(body)
} else {
if bodyFile != "" {
data, err := os.ReadFile(bodyFile)
if err != nil {
return nil, nil, err
}
bodyBytes = data
}
}
if len(bodyBytes) == 0 {
return bodyBytes, headerMap, nil
}
for k, v := range headerMap {
if strings.ToLower(k) == "content-type" && v != "" {
bodyType = strings.ToLower(v)
}
}
switch bodyType {
case BodyTypeJSON:
ok, err := isValidJSON(bodyBytes)
if err != nil {
return nil, nil, err
}
if !ok {
return nil, nil, fmt.Errorf("invalid JSON format")
}
return bodyBytes, headerMap, nil
case BodyTypeForm:
if bytes.Count(bodyBytes, []byte("=")) == 0 {
return nil, nil, fmt.Errorf("invalid body form data format")
}
return bodyBytes, headerMap, nil
case BodyTypeText:
return bodyBytes, headerMap, nil
default:
if bodyType != "" {
return bodyBytes, headerMap, nil
}
ok, err := isValidJSON(bodyBytes)
if err == nil && ok {
headerMap["Content-Type"] = BodyTypeJSON
return bodyBytes, headerMap, nil
}
equalNun := bytes.Count(bodyBytes, []byte("="))
andNun := bytes.Count(bodyBytes, []byte("&"))
if equalNun == andNun+1 {
headerMap["Content-Type"] = BodyTypeForm
} else {
headerMap["Content-Type"] = BodyTypeText
}
return bodyBytes, headerMap, nil
}
}
func trimString(s string) string {
return strings.Trim(s, " \t\r\n\"'")
}
// CheckBodyParam checks if the body parameter is provided in JSON or file format.
func CheckBodyParam(bodyJSON string, bodyFile string) (string, error) {
var body []byte

View File

@@ -59,7 +59,6 @@ func (b *Bar) shouldDraw() bool {
now := time.Now().UnixNano()
intervalNano := b.updateInterval.Nanoseconds()
//for {
lastNano := b.lastDrawNano.Load()
if now-lastNano < intervalNano {
// Too close to the last draw, skip
@@ -71,7 +70,6 @@ func (b *Bar) shouldDraw() bool {
return true
}
return false
//}
}
// draw renders the bar in the terminal.

View File

@@ -58,7 +58,7 @@ func PerfTestHTTPCMD() *cobra.Command {
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
bodyData, err := common.CheckBodyParam(body, bodyFile)
bodyBytes, headerMap, err := common.ParseHTTPParams(method, headers, body, bodyFile)
if err != nil {
return err
}
@@ -66,8 +66,8 @@ func PerfTestHTTPCMD() *cobra.Command {
params := &HTTPReqParams{
URL: targetURL,
Method: method,
Headers: headers,
Body: bodyData,
Headers: headerMap,
Body: bodyBytes,
version: "HTTP/1.1",
}

View File

@@ -58,7 +58,7 @@ func PerfTestHTTP2CMD() *cobra.Command {
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
bodyData, err := common.CheckBodyParam(body, bodyFile)
bodyBytes, headerMap, err := common.ParseHTTPParams(method, headers, body, bodyFile)
if err != nil {
return err
}
@@ -66,8 +66,8 @@ func PerfTestHTTP2CMD() *cobra.Command {
params := &HTTPReqParams{
URL: targetURL,
Method: method,
Headers: headers,
Body: bodyData,
Headers: headerMap,
Body: bodyBytes,
version: "HTTP/2",
}

View File

@@ -60,7 +60,7 @@ func PerfTestHTTP3CMD() *cobra.Command {
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
bodyData, err := common.CheckBodyParam(body, bodyFile)
bodyBytes, headerMap, err := common.ParseHTTPParams(method, headers, body, bodyFile)
if err != nil {
return err
}
@@ -68,8 +68,8 @@ func PerfTestHTTP3CMD() *cobra.Command {
params := &HTTPReqParams{
URL: targetURL,
Method: method,
Headers: headers,
Body: bodyData,
Headers: headerMap,
Body: bodyBytes,
version: "HTTP/3",
}

View File

@@ -251,8 +251,8 @@ type Result struct {
type HTTPReqParams struct {
URL string
Method string
Headers []string
Body string
Headers map[string]string
Body []byte
version string
}
@@ -263,20 +263,14 @@ func buildRequest(params *HTTPReqParams) (*http.Request, error) {
reqMethod := strings.ToUpper(params.Method)
if reqMethod == "POST" || reqMethod == "PUT" || reqMethod == "PATCH" || reqMethod == "DELETE" {
body := bytes.NewReader([]byte(params.Body))
body := bytes.NewReader(params.Body)
req, err = http.NewRequest(reqMethod, params.URL, body)
if err == nil {
req.Header.Set("Content-Type", "application/json")
}
} else {
req, err = http.NewRequest(reqMethod, params.URL, nil)
}
for _, h := range params.Headers {
kvs := strings.SplitN(h, ":", 2)
if len(kvs) == 2 {
req.Header.Set(strings.TrimSpace(kvs[0]), strings.TrimSpace(kvs[1]))
}
for k, v := range params.Headers {
req.Header.Set(k, v)
}
return req, err

View File

@@ -3,6 +3,7 @@ package http
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"math"
@@ -10,6 +11,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/fatih/color"
@@ -129,27 +131,27 @@ func (c *statsCollector) toStatistics(totalTime time.Duration, totalRequests uin
p95 = percentile(0.95)
}
var errors []string
errors := []string{}
for errStr := range c.errSet {
errors = append(errors, errStr)
}
body := params.Body
if len(body) > 300 {
body = body[:300] + "..."
body = append(body[:293], []byte(" ......")...)
}
return &Statistics{
URL: params.URL,
Method: params.Method,
Body: body,
Body: string(body),
TotalRequests: totalRequests,
Errors: errors,
SuccessCount: c.successCount,
ErrorCount: c.errorCount,
TotalTime: totalTime.Seconds(),
QPS: float64(c.successCount) / totalTime.Seconds(),
TotalDuration: math.Round(totalTime.Seconds()*100) / 100,
QPS: math.Round(float64(c.successCount)/totalTime.Seconds()*10) / 10,
AvgLatency: convertToMilliseconds(avg),
P25Latency: convertToMilliseconds(p25),
@@ -158,12 +160,29 @@ func (c *statsCollector) toStatistics(totalTime time.Duration, totalRequests uin
MinLatency: convertToMilliseconds(minLatency),
MaxLatency: convertToMilliseconds(maxLatency),
TotalSent: float64(c.totalReqBytes),
TotalReceived: float64(c.totalRespBytes),
TotalSent: c.totalReqBytes,
TotalReceived: c.totalRespBytes,
StatusCodes: c.statusCodeSet,
}
}
// convert float64 to string with specified precision, automatically process the last 0
func float64ToString(f float64, precision int) string {
if precision == 0 {
return strconv.FormatInt(int64(f), 10)
}
if precision >= 1 && precision <= 6 {
factor := math.Pow10(precision)
rounded := math.Round(f*factor) / factor
return strconv.FormatFloat(rounded, 'f', precision, 64)
}
return strconv.FormatFloat(f, 'f', -1, 64)
}
func float64ToStringNoRound(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
func (c *statsCollector) printReport(totalDuration time.Duration, totalRequests uint64, params *HTTPReqParams) (*Statistics, error) {
fmt.Printf("\n========== %s Performance Test Report ==========\n\n", params.version)
if c.successCount == 0 {
@@ -171,7 +190,7 @@ func (c *statsCollector) printReport(totalDuration time.Duration, totalRequests
fmt.Printf(" • %-19s%d\n", "Total Requests:", totalRequests)
fmt.Printf(" • %-19s%d%s\n", "Successful:", 0, color.RedString(" (0%)"))
fmt.Printf(" • %-19s%d%s\n", "Failed:", c.errorCount, color.RedString(" ✗"))
fmt.Printf(" • %-19s%.2fs\n\n", "Total Duration:", totalDuration.Seconds())
fmt.Printf(" • %-19s%s s\n\n", "Total Duration:", float64ToString(totalDuration.Seconds(), 2))
if len(c.statusCodeSet) > 0 {
printStatusCodeSet(c.statusCodeSet)
@@ -196,27 +215,28 @@ func (c *statsCollector) printReport(totalDuration time.Duration, totalRequests
if st.SuccessCount == 0 {
successStr += color.RedString(" (0%)")
} else {
successStr += color.YellowString(" (%d%%)", int(float64(st.SuccessCount)/float64(st.TotalRequests)*100))
percentage := float64ToString(float64(st.SuccessCount)/float64(st.TotalRequests)*100, 1)
successStr += color.YellowString(" (%s%%)", percentage)
}
failureStr += color.RedString(" ✗")
}
}
fmt.Println(successStr)
fmt.Println(failureStr)
fmt.Printf(" • %-19s%.2fs\n", "Total Duration:", st.TotalTime)
fmt.Printf(" • %-19s%.2f req/sec\n\n", "Throughput (QPS):", st.QPS)
fmt.Printf(" • %-19s%s s\n", "Total Duration:", float64ToStringNoRound(st.TotalDuration))
fmt.Printf(" • %-19s%s req/sec\n\n", "Throughput (QPS):", float64ToStringNoRound(st.QPS))
_, _ = color.New(color.Bold).Println("[Latency]")
fmt.Printf(" • %-19s%.2f ms\n", "Average:", st.AvgLatency)
fmt.Printf(" • %-19s%.2f ms\n", "Minimum:", st.MinLatency)
fmt.Printf(" • %-19s%.2f ms\n", "Maximum:", st.MaxLatency)
fmt.Printf(" • %-19s%.2f ms\n", "P25:", st.P25Latency)
fmt.Printf(" • %-19s%.2f ms\n", "P50:", st.P50Latency)
fmt.Printf(" • %-19s%.2f ms\n\n", "P95:", st.P95Latency)
fmt.Printf(" • %-19s%s ms\n", "Average:", float64ToStringNoRound(st.AvgLatency))
fmt.Printf(" • %-19s%s ms\n", "Minimum:", float64ToStringNoRound(st.MinLatency))
fmt.Printf(" • %-19s%s ms\n", "Maximum:", float64ToStringNoRound(st.MaxLatency))
fmt.Printf(" • %-19s%s ms\n", "P25:", float64ToStringNoRound(st.P25Latency))
fmt.Printf(" • %-19s%s ms\n", "P50:", float64ToStringNoRound(st.P50Latency))
fmt.Printf(" • %-19s%s ms\n\n", "P95:", float64ToStringNoRound(st.P95Latency))
_, _ = color.New(color.Bold).Println("[Data Transfer]")
fmt.Printf(" • %-19s%.0f Bytes\n", "Sent:", st.TotalSent)
fmt.Printf(" • %-19s%.0f Bytes\n\n", "Received:", st.TotalReceived)
fmt.Printf(" • %-19s%d Bytes\n", "Sent:", st.TotalSent)
fmt.Printf(" • %-19s%d Bytes\n\n", "Received:", st.TotalReceived)
if len(c.statusCodeSet) > 0 {
printStatusCodeSet(st.StatusCodes)
@@ -262,7 +282,7 @@ type Statistics struct {
Body string `json:"body"` // request body (JSON)
TotalRequests uint64 `json:"total_requests"` // total requests
TotalTime float64 `json:"total_time"` // seconds
TotalDuration float64 `json:"total_duration"` // total duration (seconds)
SuccessCount uint64 `json:"success_count"` // successful requests (status code 2xx)
ErrorCount uint64 `json:"error_count"` // failed requests (status code not 2xx)
Errors []string `json:"errors"` // error details
@@ -275,10 +295,12 @@ type Statistics struct {
MinLatency float64 `json:"min_latency"` // minimum latency (ms)
MaxLatency float64 `json:"max_latency"` // maximum latency (ms)
TotalSent float64 `json:"total_sent"` // total sent (bytes)
TotalReceived float64 `json:"total_received"` // total received (bytes)
TotalSent int64 `json:"total_sent"` // total sent (bytes)
TotalReceived int64 `json:"total_received"` // total received (bytes)
StatusCodes map[int]int64 `json:"status_codes"` // status code distribution (count)
CreatedAt time.Time `json:"created_at"` // created time
}
// Save saves the statistics data to a JSON file.
@@ -485,6 +507,7 @@ func (spc *statsPrometheusCollector) PushToPrometheusAsync(ctx context.Context,
func (spc *statsPrometheusCollector) PushToServer(ctx context.Context, pushURL string, elapsed time.Duration, httpReqParams *HTTPReqParams, id int64) error {
statistics := spc.statsCollector.toStatistics(elapsed, spc.statsCollector.successCount+spc.statsCollector.errorCount, httpReqParams)
statistics.PerfTestID = id
statistics.CreatedAt = time.Now()
_, err := postWithContext(ctx, pushURL, statistics)
return err
@@ -510,7 +533,11 @@ func postWithContext(ctx context.Context, url string, data *Statistics) (*http.R
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Skip certificate validation
},
}
resp, err := client.Do(req)
if err != nil {
return nil, err