feat: persist loadgen reports
This commit is contained in:
parent
1367f1b11f
commit
5ffd87f56b
@ -264,6 +264,8 @@ go run ./cmd/proxy-loadgen `
|
|||||||
凭据,也不构成 100,000 QPS 证明。限速场景以 `Requests` 表示实际发起数;当
|
凭据,也不构成 100,000 QPS 证明。限速场景以 `Requests` 表示实际发起数;当
|
||||||
`RateStartsDropped` 非零时,目标速率受压测端并发容量或时间窗限制,报告吞吐不得
|
`RateStartsDropped` 非零时,目标速率受压测端并发容量或时间窗限制,报告吞吐不得
|
||||||
标注为已达到配置的 `-rate`。
|
标注为已达到配置的 `-rate`。
|
||||||
|
需要保留单次容量证据时,使用 `-output REPORT_FILE` 同时写出 JSON 文件;文件在
|
||||||
|
同目录完整写入后才替换目标,标准输出仍保留相同报告,便于交给日志或指标系统。
|
||||||
|
|
||||||
## 关键配置与入口
|
## 关键配置与入口
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@ -41,6 +42,7 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
|
|||||||
hold := flags.Duration("hold", 0, "CONNECT tunnel hold duration; required by -scenario connect")
|
hold := flags.Duration("hold", 0, "CONNECT tunnel hold duration; required by -scenario connect")
|
||||||
extractCount := flags.Int("extract-count", 1, "proxies requested by each extract scenario request")
|
extractCount := flags.Int("extract-count", 1, "proxies requested by each extract scenario request")
|
||||||
extractFulfillment := flags.String("extract-fulfillment", "partial", "extract fulfillment: partial or allOrNothing")
|
extractFulfillment := flags.String("extract-fulfillment", "partial", "extract fulfillment: partial or allOrNothing")
|
||||||
|
outputPath := flags.String("output", "", "optional JSON report file; replaces only after a complete write")
|
||||||
var headers headerValues
|
var headers headerValues
|
||||||
flags.Var(&headers, "header", "repeatable HTTP header in Name: Value form")
|
flags.Var(&headers, "header", "repeatable HTTP header in Name: Value form")
|
||||||
if err := flags.Parse(args); err != nil {
|
if err := flags.Parse(args); err != nil {
|
||||||
@ -53,6 +55,10 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
|
|||||||
_, _ = fmt.Fprintln(stderr, "proxy-loadgen: target and a bounded workload are required")
|
_, _ = fmt.Fprintln(stderr, "proxy-loadgen: target and a bounded workload are required")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(*outputPath) != *outputPath {
|
||||||
|
_, _ = fmt.Fprintln(stderr, "proxy-loadgen: invalid output path")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
parsedHeaders, err := headers.Header()
|
parsedHeaders, err := headers.Header()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: %v\n", err)
|
||||||
@ -72,6 +78,10 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
|
|||||||
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if err := writeReportFile(*outputPath, report); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: write report: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
if err := json.NewEncoder(stdout).Encode(report); err != nil {
|
if err := json.NewEncoder(stdout).Encode(report); err != nil {
|
||||||
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: encode report: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: encode report: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
@ -79,6 +89,44 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeReportFile(path string, report loadgen.Report) error {
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content, err := json.Marshal(report)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content = append(content, '\n')
|
||||||
|
directory := filepath.Dir(path)
|
||||||
|
temporary, err := os.CreateTemp(directory, "."+filepath.Base(path)+".tmp-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temporaryPath := temporary.Name()
|
||||||
|
defer os.Remove(temporaryPath)
|
||||||
|
if err := temporary.Chmod(0o600); err != nil {
|
||||||
|
_ = temporary.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := temporary.Write(content); err != nil {
|
||||||
|
_ = temporary.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := temporary.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Prefer same-directory rename so a completed report replaces the old one
|
||||||
|
// atomically on platforms that support replacement.
|
||||||
|
if err := os.Rename(temporaryPath, path); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(temporaryPath, path)
|
||||||
|
}
|
||||||
|
|
||||||
type headerValues []string
|
type headerValues []string
|
||||||
|
|
||||||
func (values *headerValues) String() string {
|
func (values *headerValues) String() string {
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -74,3 +76,29 @@ func TestExecutePassesExtractScenario(t *testing.T) {
|
|||||||
t.Fatalf("execute() = %d; options=%+v stderr=%q", code, received, stderr.String())
|
t.Fatalf("execute() = %d; options=%+v stderr=%q", code, received, stderr.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecuteWritesReportFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "load-report.json")
|
||||||
|
if err := os.WriteFile(path, []byte("incomplete"), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(): %v", err)
|
||||||
|
}
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := execute(context.Background(), []string{
|
||||||
|
"-target", "https://target.example/health", "-requests", "1", "-concurrency", "1", "-output", path,
|
||||||
|
}, func(_ context.Context, _ loadgen.Options) (loadgen.Report, error) {
|
||||||
|
return loadgen.Report{Requests: 1, Completed: 1, Succeeded: 1, Duration: time.Second}, nil
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if code != 0 || stderr.Len() != 0 {
|
||||||
|
t.Fatalf("execute() = %d, stderr=%q", code, stderr.String())
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(): %v", err)
|
||||||
|
}
|
||||||
|
var report loadgen.Report
|
||||||
|
if err := json.Unmarshal(content, &report); err != nil || report.Succeeded != 1 || string(content) == "incomplete" {
|
||||||
|
t.Fatalf("report file = (%q, %+v, %v)", content, report, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user