mirror of
https://github.com/zhufuyi/sponge.git
synced 2025-09-26 20:51:14 +08:00
feat: adaptive processing of request body format and floating-point decimal point
This commit is contained in:
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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",
|
||||
}
|
||||
|
||||
|
@@ -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",
|
||||
}
|
||||
|
||||
|
@@ -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",
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user