提交 e220a9d6 authored 作者: 陈泽健's avatar 陈泽健

feat(performance): 预设配置登录凭据 + 增强指标体系

预设配置新增被测系统登录凭据可配置:
- 模型/Schema 新增 credential_name / login_username / login_password 字段
- 新建任务选择预设时自动填充登录凭据,任务显式传入优先
- 预设摘要展示凭据名称,菜单文案"接口预设"→"预设配置"

增强指标体系:
- 执行器采集 P95/标准差/Apdex/首字节延迟/TCP连接时间/字节数/峰值TPS/错误分类
- 模型/Schema/DB迁移补齐 performance_tasks 与 performance_snapshots 新列
- 监控面板与报告页展示新增指标
- 新增 test_performance_executor.py 单测
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 a6588716
此差异已折叠。
...@@ -161,6 +161,36 @@ async def _ensure_columns(conn) -> None: ...@@ -161,6 +161,36 @@ async def _ensure_columns(conn) -> None:
("scheduled_tasks", "case_type", "VARCHAR(20) DEFAULT 'ui'"), ("scheduled_tasks", "case_type", "VARCHAR(20) DEFAULT 'ui'"),
# 定时任务:安全测试配置ID(旧库升级) # 定时任务:安全测试配置ID(旧库升级)
("scheduled_tasks", "config_id", "VARCHAR(64) DEFAULT NULL"), ("scheduled_tasks", "config_id", "VARCHAR(64) DEFAULT NULL"),
# 性能测试:增强指标体系(P95/标准差/延迟/连接时间/带宽/峰值TPS/Apdex/错误分类,旧库升级)
("performance_tasks", "p95_response_time", "FLOAT DEFAULT 0.0"),
("performance_tasks", "std_dev", "FLOAT DEFAULT 0.0"),
("performance_tasks", "latency_avg", "FLOAT DEFAULT 0.0"),
("performance_tasks", "latency_min", "FLOAT DEFAULT 0.0"),
("performance_tasks", "latency_max", "FLOAT DEFAULT 0.0"),
("performance_tasks", "connect_time_avg", "FLOAT DEFAULT 0.0"),
("performance_tasks", "connect_time_max", "FLOAT DEFAULT 0.0"),
("performance_tasks", "peak_tps", "FLOAT DEFAULT 0.0"),
("performance_tasks", "total_sent_bytes", "INTEGER DEFAULT 0"),
("performance_tasks", "total_received_bytes", "INTEGER DEFAULT 0"),
("performance_tasks", "apdex", "FLOAT DEFAULT 1.0"),
("performance_tasks", "error_type_timeout", "INTEGER DEFAULT 0"),
("performance_tasks", "error_type_connect_error", "INTEGER DEFAULT 0"),
("performance_tasks", "error_type_client_error", "INTEGER DEFAULT 0"),
("performance_tasks", "error_type_http_4xx", "INTEGER DEFAULT 0"),
("performance_tasks", "error_type_http_5xx", "INTEGER DEFAULT 0"),
("performance_tasks", "error_type_assertion", "INTEGER DEFAULT 0"),
# 性能测试:快照增强指标(旧库升级)
("performance_snapshots", "p95_response_time", "FLOAT DEFAULT 0.0"),
("performance_snapshots", "std_dev", "FLOAT DEFAULT 0.0"),
("performance_snapshots", "latency_avg", "FLOAT DEFAULT 0.0"),
("performance_snapshots", "connect_time_avg", "FLOAT DEFAULT 0.0"),
("performance_snapshots", "sent_bytes", "INTEGER DEFAULT 0"),
("performance_snapshots", "received_bytes", "INTEGER DEFAULT 0"),
("performance_snapshots", "peak_tps", "FLOAT DEFAULT 0.0"),
# 预设配置:登录凭据名称/用户名/密码(旧库升级)
("api_presets", "credential_name", "VARCHAR(100) DEFAULT NULL"),
("api_presets", "login_username", "VARCHAR(200) DEFAULT NULL"),
("api_presets", "login_password", "VARCHAR(200) DEFAULT NULL"),
] ]
def _do_ensure(sync_conn) -> None: def _do_ensure(sync_conn) -> None:
......
...@@ -70,6 +70,17 @@ class ApiPreset(Base): ...@@ -70,6 +70,17 @@ class ApiPreset(Base):
) )
account_key: Mapped[str] = mapped_column(String(50), default="superadmin", comment="使用账号key") account_key: Mapped[str] = mapped_column(String(50), default="superadmin", comment="使用账号key")
# 登录凭据配置
credential_name: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True, default=None, comment="凭据名称,如'超级管理员'"
)
login_username: Mapped[Optional[str]] = mapped_column(
String(200), nullable=True, default=None, comment="自定义登录用户名,留空使用默认"
)
login_password: Mapped[Optional[str]] = mapped_column(
String(200), nullable=True, default=None, comment="自定义登录密码,留空使用默认"
)
# 响应捕获 # 响应捕获
capture_rules: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="捕获规则列表") capture_rules: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="捕获规则列表")
...@@ -104,6 +115,9 @@ class ApiPreset(Base): ...@@ -104,6 +115,9 @@ class ApiPreset(Base):
"auth_required": self.auth_required, "auth_required": self.auth_required,
"sign_request": self.sign_request, "sign_request": self.sign_request,
"account_key": self.account_key, "account_key": self.account_key,
"credential_name": self.credential_name,
"login_username": self.login_username,
"login_password": self.login_password,
"capture_rules": self.capture_rules or [], "capture_rules": self.capture_rules or [],
"created_at": self.created_at.isoformat() if self.created_at else None, "created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None,
......
...@@ -209,7 +209,24 @@ class PerformanceTask(Base): ...@@ -209,7 +209,24 @@ class PerformanceTask(Base):
"avg_response_time": self.avg_response_time, "avg_response_time": self.avg_response_time,
"p50_response_time": self.p50_response_time, "p50_response_time": self.p50_response_time,
"p90_response_time": self.p90_response_time, "p90_response_time": self.p90_response_time,
"p95_response_time": self.p95_response_time,
"p99_response_time": self.p99_response_time, "p99_response_time": self.p99_response_time,
"std_dev": self.std_dev,
"latency_avg": self.latency_avg,
"latency_min": self.latency_min,
"latency_max": self.latency_max,
"connect_time_avg": self.connect_time_avg,
"connect_time_max": self.connect_time_max,
"peak_tps": self.peak_tps,
"total_sent_bytes": self.total_sent_bytes,
"total_received_bytes": self.total_received_bytes,
"apdex": self.apdex,
"error_type_timeout": self.error_type_timeout,
"error_type_connect_error": self.error_type_connect_error,
"error_type_client_error": self.error_type_client_error,
"error_type_http_4xx": self.error_type_http_4xx,
"error_type_http_5xx": self.error_type_http_5xx,
"error_type_assertion": self.error_type_assertion,
"actual_tps": self.actual_tps, "actual_tps": self.actual_tps,
"error_rate": self.error_rate, "error_rate": self.error_rate,
"start_time": self.start_time.isoformat() if self.start_time else None, "start_time": self.start_time.isoformat() if self.start_time else None,
...@@ -263,6 +280,14 @@ class PerformanceSnapshot(Base): ...@@ -263,6 +280,14 @@ class PerformanceSnapshot(Base):
p50_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P50响应时间(ms)") p50_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P50响应时间(ms)")
p90_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P90响应时间(ms)") p90_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P90响应时间(ms)")
p99_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P99响应时间(ms)") p99_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P99响应时间(ms)")
# 增强指标(快照级)
p95_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P95响应时间(ms)")
std_dev: Mapped[float] = mapped_column(Float, default=0.0, comment="响应时间标准差(ms)")
latency_avg: Mapped[float] = mapped_column(Float, default=0.0, comment="平均首字节延迟(ms)")
connect_time_avg: Mapped[float] = mapped_column(Float, default=0.0, comment="平均TCP连接时间(ms)")
sent_bytes: Mapped[int] = mapped_column(Integer, default=0, comment="周期发送字节数")
received_bytes: Mapped[int] = mapped_column(Integer, default=0, comment="周期接收字节数")
peak_tps: Mapped[float] = mapped_column(Float, default=0.0, comment="当前峰值TPS")
success_count: Mapped[int] = mapped_column(Integer, default=0, comment="本周期成功数") success_count: Mapped[int] = mapped_column(Integer, default=0, comment="本周期成功数")
fail_count: Mapped[int] = mapped_column(Integer, default=0, comment="本周期失败数") fail_count: Mapped[int] = mapped_column(Integer, default=0, comment="本周期失败数")
status_2xx: Mapped[int] = mapped_column(Integer, default=0, comment="2xx计数") status_2xx: Mapped[int] = mapped_column(Integer, default=0, comment="2xx计数")
...@@ -296,7 +321,14 @@ class PerformanceSnapshot(Base): ...@@ -296,7 +321,14 @@ class PerformanceSnapshot(Base):
"max_response_time": self.max_response_time, "max_response_time": self.max_response_time,
"p50_response_time": self.p50_response_time, "p50_response_time": self.p50_response_time,
"p90_response_time": self.p90_response_time, "p90_response_time": self.p90_response_time,
"p95_response_time": self.p95_response_time,
"p99_response_time": self.p99_response_time, "p99_response_time": self.p99_response_time,
"std_dev": self.std_dev,
"latency_avg": self.latency_avg,
"connect_time_avg": self.connect_time_avg,
"sent_bytes": self.sent_bytes,
"received_bytes": self.received_bytes,
"peak_tps": self.peak_tps,
"success_count": self.success_count, "success_count": self.success_count,
"fail_count": self.fail_count, "fail_count": self.fail_count,
"status_2xx": self.status_2xx, "status_2xx": self.status_2xx,
......
...@@ -43,6 +43,11 @@ class ApiPresetCreate(BaseModel): ...@@ -43,6 +43,11 @@ class ApiPresetCreate(BaseModel):
sign_request: bool = Field(False, description="是否需要签名") sign_request: bool = Field(False, description="是否需要签名")
account_key: str = Field("superadmin", description="使用账号 key") account_key: str = Field("superadmin", description="使用账号 key")
# 登录凭据配置
credential_name: Optional[str] = Field(None, description="凭据名称,如'超级管理员'")
login_username: Optional[str] = Field(None, description="自定义登录用户名,留空使用默认")
login_password: Optional[str] = Field(None, description="自定义登录密码,留空使用默认")
# 响应捕获 # 响应捕获
capture_rules: Optional[List[CaptureRule]] = Field(None, description="捕获规则列表") capture_rules: Optional[List[CaptureRule]] = Field(None, description="捕获规则列表")
...@@ -61,6 +66,9 @@ class ApiPresetUpdate(BaseModel): ...@@ -61,6 +66,9 @@ class ApiPresetUpdate(BaseModel):
auth_required: Optional[bool] = Field(None, description="是否需要登录 Token") auth_required: Optional[bool] = Field(None, description="是否需要登录 Token")
sign_request: Optional[bool] = Field(None, description="是否需要签名") sign_request: Optional[bool] = Field(None, description="是否需要签名")
account_key: Optional[str] = Field(None, description="使用账号 key") account_key: Optional[str] = Field(None, description="使用账号 key")
credential_name: Optional[str] = Field(None, description="凭据名称")
login_username: Optional[str] = Field(None, description="自定义登录用户名")
login_password: Optional[str] = Field(None, description="自定义登录密码")
capture_rules: Optional[List[CaptureRule]] = Field(None, description="捕获规则列表") capture_rules: Optional[List[CaptureRule]] = Field(None, description="捕获规则列表")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
...@@ -81,6 +89,9 @@ class ApiPresetResponse(BaseModel): ...@@ -81,6 +89,9 @@ class ApiPresetResponse(BaseModel):
auth_required: bool = True auth_required: bool = True
sign_request: bool = False sign_request: bool = False
account_key: str = "superadmin" account_key: str = "superadmin"
credential_name: Optional[str] = None
login_username: Optional[str] = None
login_password: Optional[str] = None
capture_rules: List[Dict[str, Any]] = [] capture_rules: List[Dict[str, Any]] = []
created_at: Optional[str] = None created_at: Optional[str] = None
updated_at: Optional[str] = None updated_at: Optional[str] = None
...@@ -95,6 +106,7 @@ class ApiPresetListItem(BaseModel): ...@@ -95,6 +106,7 @@ class ApiPresetListItem(BaseModel):
description: str = "" description: str = ""
method: str = "GET" method: str = "GET"
target_url: str = "" target_url: str = ""
credential_name: Optional[str] = None
updated_at: Optional[str] = None updated_at: Optional[str] = None
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......
...@@ -166,9 +166,26 @@ class PerformanceTaskResponse(BaseModel): ...@@ -166,9 +166,26 @@ class PerformanceTaskResponse(BaseModel):
avg_response_time: float = 0.0 avg_response_time: float = 0.0
p50_response_time: float = 0.0 p50_response_time: float = 0.0
p90_response_time: float = 0.0 p90_response_time: float = 0.0
p95_response_time: float = 0.0
p99_response_time: float = 0.0 p99_response_time: float = 0.0
std_dev: float = 0.0
latency_avg: float = 0.0
latency_min: float = 0.0
latency_max: float = 0.0
connect_time_avg: float = 0.0
connect_time_max: float = 0.0
peak_tps: float = 0.0
total_sent_bytes: int = 0
total_received_bytes: int = 0
apdex: float = 1.0
actual_tps: float = 0.0 actual_tps: float = 0.0
error_rate: float = 0.0 error_rate: float = 0.0
error_type_timeout: int = 0
error_type_connect_error: int = 0
error_type_client_error: int = 0
error_type_http_4xx: int = 0
error_type_http_5xx: int = 0
error_type_assertion: int = 0
# 时间 # 时间
start_time: Optional[str] = None start_time: Optional[str] = None
...@@ -189,7 +206,9 @@ class PerformanceTaskListItem(BaseModel): ...@@ -189,7 +206,9 @@ class PerformanceTaskListItem(BaseModel):
target_url: str target_url: str
total_requests: int = 0 total_requests: int = 0
actual_tps: float = 0.0 actual_tps: float = 0.0
peak_tps: float = 0.0
avg_response_time: float = 0.0 avg_response_time: float = 0.0
p95_response_time: float = 0.0
error_rate: float = 0.0 error_rate: float = 0.0
duration_actual: float = 0.0 duration_actual: float = 0.0
start_time: Optional[str] = None start_time: Optional[str] = None
...@@ -232,7 +251,14 @@ class PerformanceSnapshotResponse(BaseModel): ...@@ -232,7 +251,14 @@ class PerformanceSnapshotResponse(BaseModel):
max_response_time: float = 0.0 max_response_time: float = 0.0
p50_response_time: float = 0.0 p50_response_time: float = 0.0
p90_response_time: float = 0.0 p90_response_time: float = 0.0
p95_response_time: float = 0.0
p99_response_time: float = 0.0 p99_response_time: float = 0.0
std_dev: float = 0.0
latency_avg: float = 0.0
connect_time_avg: float = 0.0
sent_bytes: int = 0
received_bytes: int = 0
peak_tps: float = 0.0
success_count: int = 0 success_count: int = 0
fail_count: int = 0 fail_count: int = 0
status_2xx: int = Field(0, alias="status2xx") status_2xx: int = Field(0, alias="status2xx")
...@@ -269,16 +295,35 @@ class PerformanceReportMetrics(BaseModel): ...@@ -269,16 +295,35 @@ class PerformanceReportMetrics(BaseModel):
fail_count: int = 0 fail_count: int = 0
error_rate: float = 0.0 error_rate: float = 0.0
actual_tps: float = 0.0 actual_tps: float = 0.0
peak_tps: float = 0.0
min_response_time: float = 0.0 min_response_time: float = 0.0
max_response_time: float = 0.0 max_response_time: float = 0.0
avg_response_time: float = 0.0 avg_response_time: float = 0.0
p50_response_time: float = 0.0 p50_response_time: float = 0.0
p90_response_time: float = 0.0 p90_response_time: float = 0.0
p95_response_time: float = 0.0
p99_response_time: float = 0.0 p99_response_time: float = 0.0
std_dev: float = 0.0
latency_avg: float = 0.0
latency_min: float = 0.0
latency_max: float = 0.0
connect_time_avg: float = 0.0
connect_time_max: float = 0.0
apdex: float = 1.0
total_sent_bytes: int = 0
total_received_bytes: int = 0
sent_kb_per_sec: float = 0.0
received_kb_per_sec: float = 0.0
status_2xx: int = Field(0, alias="status2xx") status_2xx: int = Field(0, alias="status2xx")
status_3xx: int = Field(0, alias="status3xx") status_3xx: int = Field(0, alias="status3xx")
status_4xx: int = Field(0, alias="status4xx") status_4xx: int = Field(0, alias="status4xx")
status_5xx: int = Field(0, alias="status5xx") status_5xx: int = Field(0, alias="status5xx")
error_type_timeout: int = 0
error_type_connect_error: int = 0
error_type_client_error: int = 0
error_type_http_4xx: int = 0
error_type_http_5xx: int = 0
error_type_assertion: int = 0
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......
...@@ -199,6 +199,11 @@ class PerformanceService: ...@@ -199,6 +199,11 @@ class PerformanceService:
data["sign_request"] = preset.sign_request data["sign_request"] = preset.sign_request
if not data.get("account_key"): if not data.get("account_key"):
data["account_key"] = preset.account_key data["account_key"] = preset.account_key
# 登录凭据:任务显式传入的优先,否则用预设值(预设留空则不覆盖)
if not data.get("login_username") and preset.login_username:
data["login_username"] = preset.login_username
if not data.get("login_password") and preset.login_password:
data["login_password"] = preset.login_password
return data return data
@staticmethod @staticmethod
...@@ -235,6 +240,11 @@ class PerformanceService: ...@@ -235,6 +240,11 @@ class PerformanceService:
else preset.sign_request else preset.sign_request
) )
data["account_key"] = data.get("account_key") or preset.account_key data["account_key"] = data.get("account_key") or preset.account_key
# 登录凭据:任务显式传入的优先,否则用预设值(预设留空则不覆盖)
if not data.get("login_username") and preset.login_username:
data["login_username"] = preset.login_username
if not data.get("login_password") and preset.login_password:
data["login_password"] = preset.login_password
return data return data
async def update_task( async def update_task(
...@@ -579,16 +589,35 @@ class PerformanceService: ...@@ -579,16 +589,35 @@ class PerformanceService:
"fail_count": task.fail_count, "fail_count": task.fail_count,
"error_rate": task.error_rate, "error_rate": task.error_rate,
"actual_tps": task.actual_tps, "actual_tps": task.actual_tps,
"peak_tps": task.peak_tps,
"min_response_time": task.min_response_time, "min_response_time": task.min_response_time,
"max_response_time": task.max_response_time, "max_response_time": task.max_response_time,
"avg_response_time": task.avg_response_time, "avg_response_time": task.avg_response_time,
"p50_response_time": task.p50_response_time, "p50_response_time": task.p50_response_time,
"p90_response_time": task.p90_response_time, "p90_response_time": task.p90_response_time,
"p95_response_time": task.p95_response_time,
"p99_response_time": task.p99_response_time, "p99_response_time": task.p99_response_time,
"std_dev": task.std_dev,
"latency_avg": task.latency_avg,
"latency_min": task.latency_min,
"latency_max": task.latency_max,
"connect_time_avg": task.connect_time_avg,
"connect_time_max": task.connect_time_max,
"apdex": task.apdex,
"total_sent_bytes": task.total_sent_bytes,
"total_received_bytes": task.total_received_bytes,
"sent_kb_per_sec": round(task.total_sent_bytes / 1024 / task.duration_actual, 2) if task.duration_actual > 0 else 0.0,
"received_kb_per_sec": round(task.total_received_bytes / 1024 / task.duration_actual, 2) if task.duration_actual > 0 else 0.0,
"status_2xx": status_2xx, "status_2xx": status_2xx,
"status_3xx": status_3xx, "status_3xx": status_3xx,
"status_4xx": status_4xx, "status_4xx": status_4xx,
"status_5xx": status_5xx, "status_5xx": status_5xx,
"error_type_timeout": task.error_type_timeout,
"error_type_connect_error": task.error_type_connect_error,
"error_type_client_error": task.error_type_client_error,
"error_type_http_4xx": task.error_type_http_4xx,
"error_type_http_5xx": task.error_type_http_5xx,
"error_type_assertion": task.error_type_assertion,
}, },
"snapshots": [s.to_dict() for s in snapshots], "snapshots": [s.to_dict() for s in snapshots],
} }
...@@ -649,7 +678,24 @@ class PerformanceService: ...@@ -649,7 +678,24 @@ class PerformanceService:
task.avg_response_time = result.get("avg_response_time", 0.0) task.avg_response_time = result.get("avg_response_time", 0.0)
task.p50_response_time = result.get("p50_response_time", 0.0) task.p50_response_time = result.get("p50_response_time", 0.0)
task.p90_response_time = result.get("p90_response_time", 0.0) task.p90_response_time = result.get("p90_response_time", 0.0)
task.p95_response_time = result.get("p95_response_time", 0.0)
task.p99_response_time = result.get("p99_response_time", 0.0) task.p99_response_time = result.get("p99_response_time", 0.0)
task.std_dev = result.get("std_dev", 0.0)
task.latency_avg = result.get("latency_avg", 0.0)
task.latency_min = result.get("latency_min", 0.0)
task.latency_max = result.get("latency_max", 0.0)
task.connect_time_avg = result.get("connect_time_avg", 0.0)
task.connect_time_max = result.get("connect_time_max", 0.0)
task.peak_tps = result.get("peak_tps", 0.0)
task.total_sent_bytes = result.get("total_sent_bytes", 0)
task.total_received_bytes = result.get("total_received_bytes", 0)
task.apdex = result.get("apdex", 1.0)
task.error_type_timeout = result.get("error_type_timeout", 0)
task.error_type_connect_error = result.get("error_type_connect_error", 0)
task.error_type_client_error = result.get("error_type_client_error", 0)
task.error_type_http_4xx = result.get("error_type_http_4xx", 0)
task.error_type_http_5xx = result.get("error_type_http_5xx", 0)
task.error_type_assertion = result.get("error_type_assertion", 0)
task.actual_tps = result.get("actual_tps", 0.0) task.actual_tps = result.get("actual_tps", 0.0)
task.error_rate = result.get("error_rate", 0.0) task.error_rate = result.get("error_rate", 0.0)
...@@ -682,7 +728,14 @@ class PerformanceService: ...@@ -682,7 +728,14 @@ class PerformanceService:
max_response_time=s.get("max_response_time", 0.0), max_response_time=s.get("max_response_time", 0.0),
p50_response_time=s.get("p50", 0.0), p50_response_time=s.get("p50", 0.0),
p90_response_time=s.get("p90", 0.0), p90_response_time=s.get("p90", 0.0),
p95_response_time=s.get("p95", 0.0),
p99_response_time=s.get("p99", 0.0), p99_response_time=s.get("p99", 0.0),
std_dev=s.get("std_dev", 0.0),
latency_avg=s.get("latency_avg", 0.0),
connect_time_avg=s.get("connect_time_avg", 0.0),
sent_bytes=s.get("sent_bytes", 0),
received_bytes=s.get("received_bytes", 0),
peak_tps=s.get("peak_tps", 0.0),
success_count=s.get("success_count", 0), success_count=s.get("success_count", 0),
fail_count=s.get("fail_count", 0), fail_count=s.get("fail_count", 0),
status_2xx=s.get("status_2xx", 0), status_2xx=s.get("status_2xx", 0),
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:test_performance_executor.py
模块描述:测试性能测试执行器与指标采集(增强指标体系)
覆盖指标:P95/标准差/Apdex/延迟/连接时间/字节数/峰值TPS/错误分类
作者:czj
创建日期:2026-08-20
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import pytest
from app.executors.performance_executor import MetricsCollector
class TestStdDev:
"""测试总体标准差计算"""
def test_less_than_two_values(self):
assert MetricsCollector._std_dev([]) == 0.0
assert MetricsCollector._std_dev([10.0]) == 0.0
def test_constant_values(self):
assert MetricsCollector._std_dev([5.0, 5.0, 5.0]) == 0.0
def test_known_values(self):
# 数据: 2, 4, 4, 4, 5, 5, 7, 9 → mean=5, variance=4, std=2.0
data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]
assert MetricsCollector._std_dev(data) == pytest.approx(2.0, abs=1e-6)
class TestApdex:
"""测试 Apdex 计算"""
def test_empty_returns_one(self):
assert MetricsCollector._apdex([]) == 1.0
def test_all_satisfied(self):
# 全部在 500ms 内 → 1.0
times = [100.0, 200.0, 300.0, 500.0]
assert MetricsCollector._apdex(times) == pytest.approx(1.0)
def test_all_tolerating(self):
# 全部在 500~2000ms 之间 → 0.5
times = [600.0, 700.0, 800.0, 1000.0]
assert MetricsCollector._apdex(times) == pytest.approx(0.5)
def test_mixed(self):
# 满意2 + 容忍1 + 超出1 → (2 + 1/2) / 4 = 0.625
times = [100.0, 300.0, 800.0, 3000.0]
assert MetricsCollector._apdex(times) == pytest.approx(0.625)
def test_custom_threshold(self):
times = [200.0, 400.0]
assert MetricsCollector._apdex(times, threshold_ms=300) == pytest.approx(0.75)
class TestPercentile:
"""测试百分位数(线性插值)"""
def test_single_value(self):
assert MetricsCollector._percentile([42.0], 95) == pytest.approx(42.0)
def test_empty(self):
assert MetricsCollector._percentile([], 95) == 0.0
def test_known_values(self):
# 数据 [1, 2, 3, 4]:P50 = 2.5(线性插值)
data = [1.0, 2.0, 3.0, 4.0]
assert MetricsCollector._percentile(data, 50) == pytest.approx(2.5)
# P100 = 4.0
assert MetricsCollector._percentile(data, 100) == pytest.approx(4.0)
def test_unsorted_input(self):
data = [4.0, 1.0, 3.0, 2.0]
assert MetricsCollector._percentile(data, 50) == pytest.approx(2.5)
class TestRecord:
"""测试 record() 指标记录与错误分类"""
def make_collector(self):
return MetricsCollector()
def test_success_record(self):
c = self.make_collector()
c.record(status=200, elapsed_ms=100.0, success=True,
latency_ms=50.0, connect_ms=10.0, sent_bytes=100, received_bytes=500)
assert c.total_requests == 1
assert c.success_count == 1
assert c.fail_count == 0
assert c.all_latencies == [50.0]
assert c.all_connect_times == [10.0]
assert c.total_sent_bytes == 100
assert c.total_received_bytes == 500
assert c.status_2xx == 1
def test_http_4xx_classification(self):
c = self.make_collector()
c.record(status=404, elapsed_ms=50.0, success=False, error='404 Not Found')
assert c.status_4xx == 1
assert c._error_counts["http_4xx"] == 1
def test_http_5xx_classification(self):
c = self.make_collector()
c.record(status=500, elapsed_ms=50.0, success=False, error='500 Server Error')
assert c.status_5xx == 1
assert c._error_counts["http_5xx"] == 1
def test_assertion_fail_classification(self):
# 状态码成功但断言未通过 → assertion_fail
c = self.make_collector()
c.record(status=200, elapsed_ms=50.0, success=False, error='断言失败: 返回值不匹配')
assert c._error_counts["assertion_fail"] == 1
def test_timeout_classification(self):
c = self.make_collector()
c.record(status=0, elapsed_ms=3000.0, success=False, error='timeout', error_type='timeout')
assert c._error_counts["timeout"] == 1
def test_connect_error_classification(self):
c = self.make_collector()
c.record(status=0, elapsed_ms=1.0, success=False, error='connect error', error_type='connect_error')
assert c._error_counts["connect_error"] == 1
class TestSnapshot:
"""测试 snapshot() 周期快照"""
def test_empty_snapshot(self):
c = MetricsCollector()
snap = c.snapshot(peak_tps=3.5, concurrent_users=2)
assert snap["elapsed"] >= 0
assert snap["tps"] == 0
assert snap["std_dev"] == 0
assert snap["latency_avg"] == 0
assert snap["connect_time_avg"] == 0
assert snap["sent_bytes"] == 0
assert snap["received_bytes"] == 0
assert snap["peak_tps"] == 3.5
assert snap["concurrent_users"] == 2
def test_snapshot_fields_and_reset(self):
c = MetricsCollector()
c.record(status=200, elapsed_ms=100.0, success=True, latency_ms=40.0,
connect_ms=5.0, sent_bytes=200, received_bytes=1000)
c.record(status=200, elapsed_ms=300.0, success=True, latency_ms=120.0,
connect_ms=15.0, sent_bytes=200, received_bytes=1000)
snap = c.snapshot(peak_tps=1.0, concurrent_users=1)
assert snap["tps"] == 2.0
# 平均响应时间 (100+300)/2
assert snap["avg_response_time"] == pytest.approx(200.0)
# 标准差 (100-200)^2 + (300-200)^2 → /2 → sqrt(10000) = 100
assert snap["std_dev"] == pytest.approx(100.0)
assert snap["latency_avg"] == pytest.approx(80.0)
assert snap["connect_time_avg"] == pytest.approx(10.0)
assert snap["sent_bytes"] == 400
assert snap["received_bytes"] == 2000
assert snap["p95"] == pytest.approx(290.0)
# 周期计数器已重置
assert c._period_success == 0
assert c._period_times == []
assert c._period_sent_bytes == 0
# 全局累计保留
assert c.total_requests == 2
assert c.total_sent_bytes == 400
def test_snapshot_multiple_periods(self):
c = MetricsCollector()
c.record(status=200, elapsed_ms=100.0, success=True)
c.snapshot()
c.record(status=200, elapsed_ms=200.0, success=True)
snap = c.snapshot()
assert snap["tps"] == 1.0
assert snap["avg_response_time"] == pytest.approx(200.0)
assert c.total_requests == 2
class TestSummary:
"""测试 summary() 最终汇总"""
def test_empty_summary(self):
c = MetricsCollector()
s = c.summary()
assert s["total_requests"] == 0
assert s["error_rate"] == 0.0
assert s["p95_response_time"] == 0
assert s["std_dev"] == 0
assert s["apdex"] == pytest.approx(1.0)
assert s["error_type_timeout"] == 0
def test_summary_aggregation(self):
c = MetricsCollector()
# 100ms 成功 + 600ms 成功 + 5000ms 超时
c.record(status=200, elapsed_ms=100.0, success=True, latency_ms=40.0, connect_ms=5.0,
sent_bytes=100, received_bytes=1000)
c.record(status=200, elapsed_ms=600.0, success=True, latency_ms=200.0, connect_ms=20.0,
sent_bytes=100, received_bytes=2000)
c.record(status=0, elapsed_ms=5000.0, success=False, error='timeout', error_type='timeout',
latency_ms=5000.0, connect_ms=10.0, sent_bytes=100, received_bytes=0)
s = c.summary()
assert s["total_requests"] == 3
assert s["success_count"] == 2
assert s["fail_count"] == 1
assert s["error_rate"] == pytest.approx(100 / 3, abs=0.01)
assert s["p50_response_time"] == pytest.approx(600.0)
# 线性插值 P95(数据 [100, 600, 5000]):600 + (5000-600) * 0.9
assert s["p95_response_time"] == pytest.approx(4560.0)
assert s["latency_avg"] == pytest.approx((40 + 200 + 5000) / 3, abs=0.01)
assert s["connect_time_avg"] == pytest.approx((5 + 20 + 10) / 3, abs=0.01)
assert s["total_sent_bytes"] == 300
assert s["total_received_bytes"] == 3000
assert s["error_type_timeout"] == 1
# Apdex:满意1(100) + 容忍1(600) + 超出1(5000) → (1 + 0.5) / 3 = 0.5
assert s["apdex"] == pytest.approx(0.5, abs=0.01)
def test_summary_error_types(self):
c = MetricsCollector()
c.record(status=500, elapsed_ms=100.0, success=False, error='500')
c.record(status=404, elapsed_ms=100.0, success=False, error='404')
c.record(status=200, elapsed_ms=100.0, success=False, error='断言失败')
c.record(status=0, elapsed_ms=100.0, success=False, error='conn', error_type='connect_error')
s = c.summary()
assert s["error_type_http_5xx"] == 1
assert s["error_type_http_4xx"] == 1
assert s["error_type_assertion"] == 1
assert s["error_type_connect_error"] == 1
assert s["fail_count"] == 4
def test_bytes_kb_per_sec(self):
c = MetricsCollector()
c.record(status=200, elapsed_ms=100.0, success=True, sent_bytes=10240, received_bytes=5120)
s = c.summary()
# elapsed = max(实际耗时, 1s),测试进程远小于 1s → 速率为 10/1 = 10 KB/s;
# 慢机器上耗时略超 1s 时速率相应变小,故断言上界
assert s["sent_kb_per_sec"] > 0
assert s["sent_kb_per_sec"] <= 10.0
assert s["received_kb_per_sec"] > 0
assert s["received_kb_per_sec"] <= 5.0
\ No newline at end of file
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
<el-menu-item index="/performance/tasks">任务管理</el-menu-item> <el-menu-item index="/performance/tasks">任务管理</el-menu-item>
<el-menu-item index="/performance/monitor">执行监控</el-menu-item> <el-menu-item index="/performance/monitor">执行监控</el-menu-item>
<el-menu-item index="/performance/report">报告查看</el-menu-item> <el-menu-item index="/performance/report">报告查看</el-menu-item>
<el-menu-item index="/performance/presets">接口预设</el-menu-item> <el-menu-item index="/performance/presets">预设配置</el-menu-item>
</el-sub-menu> </el-sub-menu>
<!-- 接口测试(一级菜单,含用例管理/执行中心/报告中心) --> <!-- 接口测试(一级菜单,含用例管理/执行中心/报告中心) -->
...@@ -283,7 +283,7 @@ const PERFORMANCE_LABELS: Record<string, string> = { ...@@ -283,7 +283,7 @@ const PERFORMANCE_LABELS: Record<string, string> = {
tasks: '任务管理', tasks: '任务管理',
monitor: '执行监控', monitor: '执行监控',
report: '报告查看', report: '报告查看',
presets: '接口预设', presets: '预设配置',
} }
/** 功能测试报告子页面标签映射 */ /** 功能测试报告子页面标签映射 */
......
...@@ -185,7 +185,7 @@ const routes: RouteRecordRaw[] = [ ...@@ -185,7 +185,7 @@ const routes: RouteRecordRaw[] = [
path: 'presets', path: 'presets',
name: 'PerfPresets', name: 'PerfPresets',
component: () => import('@/views/performance/ApiPresetList.vue'), component: () => import('@/views/performance/ApiPresetList.vue'),
meta: { title: '接口预设' } meta: { title: '预设配置' }
} }
] ]
}, },
......
...@@ -66,7 +66,24 @@ export interface PerformanceTask { ...@@ -66,7 +66,24 @@ export interface PerformanceTask {
avgResponseTime: number | null avgResponseTime: number | null
p50ResponseTime: number | null p50ResponseTime: number | null
p90ResponseTime: number | null p90ResponseTime: number | null
p95ResponseTime: number | null
p99ResponseTime: number | null p99ResponseTime: number | null
stdDev: number | null
latencyAvg: number | null
latencyMin: number | null
latencyMax: number | null
connectTimeAvg: number | null
connectTimeMax: number | null
peakTps: number | null
totalSentBytes: number | null
totalReceivedBytes: number | null
apdex: number | null
errorTypeTimeout: number | null
errorTypeConnectError: number | null
errorTypeClientError: number | null
errorTypeHttp4xx: number | null
errorTypeHttp5xx: number | null
errorTypeAssertion: number | null
actualTps: number | null actualTps: number | null
errorRate: number | null errorRate: number | null
startTime: string | null startTime: string | null
...@@ -141,6 +158,9 @@ export interface PerformanceTaskListItem { ...@@ -141,6 +158,9 @@ export interface PerformanceTaskListItem {
concurrency: number concurrency: number
duration: number duration: number
actualTps: number | null actualTps: number | null
peakTps: number | null
p95ResponseTime: number | null
avgResponseTime: number | null
errorRate: number | null errorRate: number | null
startTime: string | null startTime: string | null
endTime: string | null endTime: string | null
...@@ -177,7 +197,14 @@ export interface PerformanceSnapshot { ...@@ -177,7 +197,14 @@ export interface PerformanceSnapshot {
maxResponseTime: number maxResponseTime: number
p50ResponseTime: number p50ResponseTime: number
p90ResponseTime: number p90ResponseTime: number
p95ResponseTime: number
p99ResponseTime: number p99ResponseTime: number
stdDev: number
latencyAvg: number
connectTimeAvg: number
sentBytes: number
receivedBytes: number
peakTps: number
successCount: number successCount: number
failCount: number failCount: number
status2xx: number status2xx: number
...@@ -214,16 +241,35 @@ export interface PerformanceReportMetrics { ...@@ -214,16 +241,35 @@ export interface PerformanceReportMetrics {
failCount: number failCount: number
errorRate: number errorRate: number
actualTps: number actualTps: number
peakTps: number
minResponseTime: number minResponseTime: number
maxResponseTime: number maxResponseTime: number
avgResponseTime: number avgResponseTime: number
p50ResponseTime: number p50ResponseTime: number
p90ResponseTime: number p90ResponseTime: number
p95ResponseTime: number
p99ResponseTime: number p99ResponseTime: number
stdDev: number
latencyAvg: number
latencyMin: number
latencyMax: number
connectTimeAvg: number
connectTimeMax: number
apdex: number
totalSentBytes: number
totalReceivedBytes: number
sentKbPerSec: number
receivedKbPerSec: number
status2xx: number status2xx: number
status3xx: number status3xx: number
status4xx: number status4xx: number
status5xx: number status5xx: number
errorTypeTimeout: number
errorTypeConnectError: number
errorTypeClientError: number
errorTypeHttp4xx: number
errorTypeHttp5xx: number
errorTypeAssertion: number
} }
/** 报告响应 */ /** 报告响应 */
...@@ -243,10 +289,18 @@ export interface PerfWsSnapshot { ...@@ -243,10 +289,18 @@ export interface PerfWsSnapshot {
avgResponseTime: number avgResponseTime: number
p50: number p50: number
p90: number p90: number
p95: number
p99: number p99: number
stdDev: number
latencyAvg: number
connectTimeAvg: number
sentBytes: number
receivedBytes: number
peakTps: number
successCount: number successCount: number
failCount: number failCount: number
totalRequests: number totalRequests: number
concurrentUsers: number
status2xx: number status2xx: number
status4xx: number status4xx: number
status5xx: number status5xx: number
...@@ -277,6 +331,9 @@ export interface ApiPreset { ...@@ -277,6 +331,9 @@ export interface ApiPreset {
authRequired: boolean authRequired: boolean
signRequest: boolean signRequest: boolean
accountKey: string accountKey: string
credentialName: string | null
loginUsername: string | null
loginPassword: string | null
captureRules: any[] | null captureRules: any[] | null
createdAt: string | null createdAt: string | null
updatedAt: string | null updatedAt: string | null
...@@ -294,6 +351,9 @@ export interface ApiPresetCreate { ...@@ -294,6 +351,9 @@ export interface ApiPresetCreate {
authRequired?: boolean authRequired?: boolean
signRequest?: boolean signRequest?: boolean
accountKey?: string accountKey?: string
credentialName?: string | null
loginUsername?: string | null
loginPassword?: string | null
captureRules?: any[] | null captureRules?: any[] | null
} }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<template> <template>
<el-dialog <el-dialog
v-model="visible" v-model="visible"
:title="isEdit ? '编辑接口预设' : '新建接口预设'" :title="isEdit ? '编辑预设配置' : '新建预设配置'"
width="680px" width="680px"
top="3vh" top="3vh"
:close-on-click-modal="false" :close-on-click-modal="false"
...@@ -107,6 +107,28 @@ ...@@ -107,6 +107,28 @@
</el-col> </el-col>
</el-row> </el-row>
<el-divider v-if="form.authRequired" style="margin: 8px 0" />
<template v-if="form.authRequired">
<div class="credential-section-label" style="font-size: 13px; color: #909399; margin-bottom: 8px;">登录凭据(可选,留空使用默认值)</div>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="凭据名称">
<el-input v-model="form.credentialName" placeholder="如:超级管理员" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="登录用户名">
<el-input v-model="form.loginUsername" placeholder="留空使用默认" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="登录密码">
<el-input v-model="form.loginPassword" type="password" show-password placeholder="留空使用默认" clearable />
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 描述 --> <!-- 描述 -->
<el-form-item label="描述"> <el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="预设描述(可选)" /> <el-input v-model="form.description" type="textarea" :rows="2" placeholder="预设描述(可选)" />
...@@ -163,6 +185,9 @@ interface PresetForm { ...@@ -163,6 +185,9 @@ interface PresetForm {
authRequired: boolean authRequired: boolean
signRequest: boolean signRequest: boolean
accountKey: string accountKey: string
credentialName: string
loginUsername: string
loginPassword: string
} }
const defaultForm = (): PresetForm => ({ const defaultForm = (): PresetForm => ({
...@@ -177,6 +202,9 @@ const defaultForm = (): PresetForm => ({ ...@@ -177,6 +202,9 @@ const defaultForm = (): PresetForm => ({
authRequired: true, authRequired: true,
signRequest: false, signRequest: false,
accountKey: 'superadmin', accountKey: 'superadmin',
credentialName: '',
loginUsername: '',
loginPassword: '',
}) })
const form = reactive<PresetForm>(defaultForm()) const form = reactive<PresetForm>(defaultForm())
...@@ -224,6 +252,9 @@ function initForm() { ...@@ -224,6 +252,9 @@ function initForm() {
form.authRequired = p.authRequired form.authRequired = p.authRequired
form.signRequest = p.signRequest form.signRequest = p.signRequest
form.accountKey = p.accountKey || 'superadmin' form.accountKey = p.accountKey || 'superadmin'
form.credentialName = p.credentialName || ''
form.loginUsername = p.loginUsername || ''
form.loginPassword = p.loginPassword || ''
} else { } else {
isEdit.value = false isEdit.value = false
Object.assign(form, defaultForm()) Object.assign(form, defaultForm())
...@@ -255,6 +286,9 @@ async function handleSave() { ...@@ -255,6 +286,9 @@ async function handleSave() {
authRequired: form.authRequired, authRequired: form.authRequired,
signRequest: form.signRequest, signRequest: form.signRequest,
accountKey: form.authRequired ? form.accountKey : null, accountKey: form.authRequired ? form.accountKey : null,
credentialName: form.credentialName || null,
loginUsername: form.loginUsername || null,
loginPassword: form.loginPassword || null,
} }
if (form.bodyType === 'static' && form.body) { if (form.bodyType === 'static' && form.body) {
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
<!-- 提示 --> <!-- 提示 -->
<el-alert <el-alert
title="接口预设用于快速填充任务配置。修改或删除预设不影响已创建的任务。" title="预设配置用于快速填充任务配置,修改或删除预设不影响已创建的任务。"
type="info" type="info"
:closable="false" :closable="false"
show-icon show-icon
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
<el-tag :type="methodTagType(row.method)" size="small">{{ row.method }}</el-tag> <el-tag :type="methodTagType(row.method)" size="small">{{ row.method }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="credentialName" label="凭据名称" width="150" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.credentialName || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="targetUrl" label="目标 URL" min-width="300" show-overflow-tooltip /> <el-table-column prop="targetUrl" label="目标 URL" min-width="300" show-overflow-tooltip />
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip> <el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
......
...@@ -55,25 +55,25 @@ ...@@ -55,25 +55,25 @@
<!-- 实时指标卡片 --> <!-- 实时指标卡片 -->
<el-row :gutter="12" class="metrics-row"> <el-row :gutter="12" class="metrics-row">
<el-col :span="6"> <el-col :span="4">
<el-card shadow="never" class="metric-card"> <el-card shadow="never" class="metric-card">
<div class="metric-value">{{ currentTps.toFixed(1) }}</div> <div class="metric-value">{{ currentTps.toFixed(1) }}</div>
<div class="metric-label">当前 TPS</div> <div class="metric-label">当前 TPS</div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="4">
<el-card shadow="never" class="metric-card"> <el-card shadow="never" class="metric-card">
<div class="metric-value">{{ currentAvgRt.toFixed(0) }}ms</div> <div class="metric-value">{{ currentAvgRt.toFixed(0) }}ms</div>
<div class="metric-label">平均响应时间</div> <div class="metric-label">平均响应时间</div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="4">
<el-card shadow="never" class="metric-card"> <el-card shadow="never" class="metric-card">
<div class="metric-value">{{ totalRequests }}</div> <div class="metric-value">{{ totalRequests }}</div>
<div class="metric-label">总请求数</div> <div class="metric-label">总请求数</div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="4">
<el-card shadow="never" class="metric-card"> <el-card shadow="never" class="metric-card">
<div class="metric-value" :style="{ color: errorRate > 5 ? '#f56c6c' : '#67c23a' }"> <div class="metric-value" :style="{ color: errorRate > 5 ? '#f56c6c' : '#67c23a' }">
{{ errorRate.toFixed(1) }}% {{ errorRate.toFixed(1) }}%
...@@ -81,6 +81,18 @@ ...@@ -81,6 +81,18 @@
<div class="metric-label">错误率</div> <div class="metric-label">错误率</div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="4">
<el-card shadow="never" class="metric-card">
<div class="metric-value">{{ currentPeakTps.toFixed(1) }}</div>
<div class="metric-label">峰值 TPS</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="never" class="metric-card">
<div class="metric-value">{{ currentInflight }}</div>
<div class="metric-label">实时并发数</div>
</el-card>
</el-col>
</el-row> </el-row>
<!-- TPS 趋势图 --> <!-- TPS 趋势图 -->
...@@ -93,7 +105,7 @@ ...@@ -93,7 +105,7 @@
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :span="16"> <el-col :span="16">
<el-card shadow="never" class="chart-card"> <el-card shadow="never" class="chart-card">
<template #header><span>响应时间趋势</span></template> <template #header><span>响应时间趋势(含 P95)</span></template>
<div ref="rtChartRef" class="chart" /> <div ref="rtChartRef" class="chart" />
</el-card> </el-card>
</el-col> </el-col>
...@@ -104,6 +116,22 @@ ...@@ -104,6 +116,22 @@
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<!-- 延迟/带宽 + 并发 -->
<el-row :gutter="12">
<el-col :span="16">
<el-card shadow="never" class="chart-card">
<template #header><span>延迟与连接时间趋势</span></template>
<div ref="latencyChartRef" class="chart" />
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="chart-card">
<template #header><span>并发数趋势</span></template>
<div ref="inflightChartRef" class="chart" />
</el-card>
</el-col>
</el-row>
</template> </template>
</div> </div>
</template> </template>
...@@ -165,6 +193,8 @@ const taskStatus = ref<string>('') ...@@ -165,6 +193,8 @@ const taskStatus = ref<string>('')
// 实时指标 // 实时指标
const currentTps = ref(0) const currentTps = ref(0)
const currentAvgRt = ref(0) const currentAvgRt = ref(0)
const currentPeakTps = ref(0)
const currentInflight = ref(0)
const totalRequests = ref(0) const totalRequests = ref(0)
const errorRate = ref(0) const errorRate = ref(0)
...@@ -174,7 +204,11 @@ const tpsData = ref<number[]>([]) ...@@ -174,7 +204,11 @@ const tpsData = ref<number[]>([])
const avgRtData = ref<number[]>([]) const avgRtData = ref<number[]>([])
const p50Data = ref<number[]>([]) const p50Data = ref<number[]>([])
const p90Data = ref<number[]>([]) const p90Data = ref<number[]>([])
const p95Data = ref<number[]>([])
const p99Data = ref<number[]>([]) const p99Data = ref<number[]>([])
const latencyAvgData = ref<number[]>([])
const connectTimeAvgData = ref<number[]>([])
const inflightData = ref<number[]>([])
const status2xx = ref(0) const status2xx = ref(0)
const status4xx = ref(0) const status4xx = ref(0)
const status5xx = ref(0) const status5xx = ref(0)
...@@ -183,10 +217,14 @@ const status5xx = ref(0) ...@@ -183,10 +217,14 @@ const status5xx = ref(0)
const tpsChartRef = ref<HTMLElement>() const tpsChartRef = ref<HTMLElement>()
const rtChartRef = ref<HTMLElement>() const rtChartRef = ref<HTMLElement>()
const statusChartRef = ref<HTMLElement>() const statusChartRef = ref<HTMLElement>()
const latencyChartRef = ref<HTMLElement>()
const inflightChartRef = ref<HTMLElement>()
let tpsChart: echarts.ECharts | null = null let tpsChart: echarts.ECharts | null = null
let rtChart: echarts.ECharts | null = null let rtChart: echarts.ECharts | null = null
let statusChart: echarts.ECharts | null = null let statusChart: echarts.ECharts | null = null
let latencyChart: echarts.ECharts | null = null
let inflightChart: echarts.ECharts | null = null
let ws: WebSocket | null = null let ws: WebSocket | null = null
/** 状态标签类型 */ /** 状态标签类型 */
...@@ -212,6 +250,12 @@ function initCharts() { ...@@ -212,6 +250,12 @@ function initCharts() {
if (statusChartRef.value) { if (statusChartRef.value) {
statusChart = echarts.init(statusChartRef.value) statusChart = echarts.init(statusChartRef.value)
} }
if (latencyChartRef.value) {
latencyChart = echarts.init(latencyChartRef.value)
}
if (inflightChartRef.value) {
inflightChart = echarts.init(inflightChartRef.value)
}
} }
/** 更新 TPS 图表 */ /** 更新 TPS 图表 */
...@@ -244,7 +288,7 @@ function updateRtChart() { ...@@ -244,7 +288,7 @@ function updateRtChart() {
if (!rtChart) return if (!rtChart) return
const option: EChartsOption = { const option: EChartsOption = {
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { data: ['平均', 'P50', 'P90', 'P99'], top: 0, itemWidth: 12, itemHeight: 8 }, legend: { data: ['平均', 'P50', 'P90', 'P95', 'P99'], top: 0, itemWidth: 12, itemHeight: 8 },
grid: { left: 50, right: 20, bottom: 30, top: 30 }, grid: { left: 50, right: 20, bottom: 30, top: 30 },
xAxis: { type: 'category', data: timeLabels.value, axisLabel: { fontSize: 11 } }, xAxis: { type: 'category', data: timeLabels.value, axisLabel: { fontSize: 11 } },
yAxis: { type: 'value', name: 'ms' }, yAxis: { type: 'value', name: 'ms' },
...@@ -252,12 +296,51 @@ function updateRtChart() { ...@@ -252,12 +296,51 @@ function updateRtChart() {
{ name: '平均', type: 'line', data: avgRtData.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5 } }, { name: '平均', type: 'line', data: avgRtData.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5 } },
{ name: 'P50', type: 'line', data: p50Data.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dashed' } }, { name: 'P50', type: 'line', data: p50Data.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dashed' } },
{ name: 'P90', type: 'line', data: p90Data.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dashed' } }, { name: 'P90', type: 'line', data: p90Data.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dashed' } },
{ name: 'P95', type: 'line', data: p95Data.value, smooth: true, showSymbol: false, lineStyle: { width: 2, type: 'dashed', color: '#e6a23c' } },
{ name: 'P99', type: 'line', data: p99Data.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dotted' } }, { name: 'P99', type: 'line', data: p99Data.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dotted' } },
], ],
} }
rtChart.setOption(option, true) rtChart.setOption(option, true)
} }
/** 更新延迟与连接时间图表 */
function updateLatencyChart() {
if (!latencyChart) return
const option: EChartsOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['首字节延迟', '连接时间'], top: 0, itemWidth: 12, itemHeight: 8 },
grid: { left: 50, right: 20, bottom: 30, top: 30 },
xAxis: { type: 'category', data: timeLabels.value, axisLabel: { fontSize: 11 } },
yAxis: { type: 'value', name: 'ms' },
series: [
{ name: '首字节延迟', type: 'line', data: latencyAvgData.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5 }, areaStyle: { opacity: 0.08 } },
{ name: '连接时间', type: 'line', data: connectTimeAvgData.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, type: 'dashed', color: '#e6a23c' } },
],
}
latencyChart.setOption(option, true)
}
/** 更新并发数图表 */
function updateInflightChart() {
if (!inflightChart) return
const option: EChartsOption = {
tooltip: { trigger: 'axis' },
grid: { left: 50, right: 20, bottom: 30, top: 10 },
xAxis: { type: 'category', data: timeLabels.value, axisLabel: { fontSize: 11 } },
yAxis: { type: 'value', name: '并发数', minInterval: 1 },
series: [{
type: 'line',
data: inflightData.value,
smooth: true,
showSymbol: false,
step: 'end',
lineStyle: { width: 2, color: '#9b59b6' },
areaStyle: { opacity: 0.12 },
}],
}
inflightChart.setOption(option, true)
}
/** 更新状态码饼图 */ /** 更新状态码饼图 */
function updateStatusChart() { function updateStatusChart() {
if (!statusChart) return if (!statusChart) return
...@@ -283,6 +366,8 @@ function handleSnapshot(data: any) { ...@@ -283,6 +366,8 @@ function handleSnapshot(data: any) {
const d = data.data || data const d = data.data || data
currentTps.value = d.tps || 0 currentTps.value = d.tps || 0
currentAvgRt.value = d.avgResponseTime || d.avg_response_time || 0 currentAvgRt.value = d.avgResponseTime || d.avg_response_time || 0
currentPeakTps.value = d.peakTps || d.peak_tps || 0
currentInflight.value = d.concurrentUsers || d.concurrent_users || 0
totalRequests.value = d.totalRequests || d.total_requests || 0 totalRequests.value = d.totalRequests || d.total_requests || 0
const fail = d.failCount || d.fail_count || 0 const fail = d.failCount || d.fail_count || 0
const total = totalRequests.value const total = totalRequests.value
...@@ -295,7 +380,11 @@ function handleSnapshot(data: any) { ...@@ -295,7 +380,11 @@ function handleSnapshot(data: any) {
avgRtData.value.push(d.avgResponseTime || d.avg_response_time || 0) avgRtData.value.push(d.avgResponseTime || d.avg_response_time || 0)
p50Data.value.push(d.p50 || 0) p50Data.value.push(d.p50 || 0)
p90Data.value.push(d.p90 || 0) p90Data.value.push(d.p90 || 0)
p95Data.value.push(d.p95 || 0)
p99Data.value.push(d.p99 || 0) p99Data.value.push(d.p99 || 0)
latencyAvgData.value.push(d.latencyAvg || d.latency_avg || 0)
connectTimeAvgData.value.push(d.connectTimeAvg || d.connect_time_avg || 0)
inflightData.value.push(d.concurrentUsers || d.concurrent_users || 0)
status2xx.value += d.status2xx || d.status_2xx || 0 status2xx.value += d.status2xx || d.status_2xx || 0
status4xx.value += d.status4xx || d.status_4xx || 0 status4xx.value += d.status4xx || d.status_4xx || 0
...@@ -308,13 +397,19 @@ function handleSnapshot(data: any) { ...@@ -308,13 +397,19 @@ function handleSnapshot(data: any) {
avgRtData.value.shift() avgRtData.value.shift()
p50Data.value.shift() p50Data.value.shift()
p90Data.value.shift() p90Data.value.shift()
p95Data.value.shift()
p99Data.value.shift() p99Data.value.shift()
latencyAvgData.value.shift()
connectTimeAvgData.value.shift()
inflightData.value.shift()
} }
// 更新图表 // 更新图表
updateTpsChart() updateTpsChart()
updateRtChart() updateRtChart()
updateStatusChart() updateStatusChart()
updateLatencyChart()
updateInflightChart()
taskStatus.value = 'running' taskStatus.value = 'running'
} }
...@@ -338,12 +433,18 @@ function connectWs() { ...@@ -338,12 +433,18 @@ function connectWs() {
avgRtData.value = [] avgRtData.value = []
p50Data.value = [] p50Data.value = []
p90Data.value = [] p90Data.value = []
p95Data.value = []
p99Data.value = [] p99Data.value = []
latencyAvgData.value = []
connectTimeAvgData.value = []
inflightData.value = []
status2xx.value = 0 status2xx.value = 0
status4xx.value = 0 status4xx.value = 0
status5xx.value = 0 status5xx.value = 0
currentTps.value = 0 currentTps.value = 0
currentAvgRt.value = 0 currentAvgRt.value = 0
currentPeakTps.value = 0
currentInflight.value = 0
totalRequests.value = 0 totalRequests.value = 0
errorRate.value = 0 errorRate.value = 0
taskStatus.value = '' taskStatus.value = ''
...@@ -411,6 +512,8 @@ onUnmounted(() => { ...@@ -411,6 +512,8 @@ onUnmounted(() => {
tpsChart?.dispose() tpsChart?.dispose()
rtChart?.dispose() rtChart?.dispose()
statusChart?.dispose() statusChart?.dispose()
latencyChart?.dispose()
inflightChart?.dispose()
}) })
</script> </script>
......
...@@ -138,7 +138,7 @@ ...@@ -138,7 +138,7 @@
<div v-if="selectedPreset.description"><strong>描述:</strong>{{ selectedPreset.description }}</div> <div v-if="selectedPreset.description"><strong>描述:</strong>{{ selectedPreset.description }}</div>
<div> <div>
<strong>认证:</strong> <strong>认证:</strong>
{{ selectedPreset.authRequired ? '需要登录' : '无需登录' }}<template v-if="selectedPreset.authRequired">{{ accountKeyLabel(selectedPreset.accountKey) }}</template> {{ selectedPreset.authRequired ? '需要登录' : '无需登录' }}<template v-if="selectedPreset.authRequired">{{ selectedPreset.credentialName || accountKeyLabel(selectedPreset.accountKey) }}</template>
· 签名:{{ selectedPreset.signRequest ? '需要' : '不需要' }} · 签名:{{ selectedPreset.signRequest ? '需要' : '不需要' }}
</div> </div>
<el-button link type="primary" size="small" class="preset-summary-clear" @click="onPresetChange('')"> <el-button link type="primary" size="small" class="preset-summary-clear" @click="onPresetChange('')">
...@@ -593,6 +593,8 @@ async function onPresetChange(presetId: string) { ...@@ -593,6 +593,8 @@ async function onPresetChange(presetId: string) {
form.authRequired = preset.authRequired form.authRequired = preset.authRequired
form.signRequest = preset.signRequest form.signRequest = preset.signRequest
form.accountKey = preset.accountKey || 'superadmin' form.accountKey = preset.accountKey || 'superadmin'
form.loginUsername = preset.loginUsername || ''
form.loginPassword = preset.loginPassword || ''
form.presetId = preset.id form.presetId = preset.id
} catch (e: any) { } catch (e: any) {
ElMessage.error('获取预设详情失败: ' + (e.message || '')) ElMessage.error('获取预设详情失败: ' + (e.message || ''))
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论