feat: persist loadgen reports

This commit is contained in:
youfak 2026-08-07 15:46:10 +08:00
parent 1367f1b11f
commit 5ffd87f56b
3 changed files with 78 additions and 0 deletions

View File

@ -264,6 +264,8 @@ go run ./cmd/proxy-loadgen `
凭据,也不构成 100,000 QPS 证明。限速场景以 `Requests` 表示实际发起数;当
`RateStartsDropped` 非零时,目标速率受压测端并发容量或时间窗限制,报告吞吐不得
标注为已达到配置的 `-rate`
需要保留单次容量证据时,使用 `-output REPORT_FILE` 同时写出 JSON 文件;文件在
同目录完整写入后才替换目标,标准输出仍保留相同报告,便于交给日志或指标系统。
## 关键配置与入口

View File

@ -10,6 +10,7 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"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")
extractCount := flags.Int("extract-count", 1, "proxies requested by each extract scenario request")
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
flags.Var(&headers, "header", "repeatable HTTP header in Name: Value form")
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")
return 2
}
if strings.TrimSpace(*outputPath) != *outputPath {
_, _ = fmt.Fprintln(stderr, "proxy-loadgen: invalid output path")
return 2
}
parsedHeaders, err := headers.Header()
if err != nil {
_, _ = 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)
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 {
_, _ = fmt.Fprintf(stderr, "proxy-loadgen: encode report: %v\n", err)
return 1
@ -79,6 +89,44 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
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
func (values *headerValues) String() string {

View File

@ -4,6 +4,8 @@ import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
@ -74,3 +76,29 @@ func TestExecutePassesExtractScenario(t *testing.T) {
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)
}
}