f481bcd396
新增 render_metrics.py,提供 Prometheus 指标埋点: - render_task_total: 任务总数(按引擎/任务类型/状态) - render_task_duration_seconds: 耗时直方图(12个bucket,1s~2h) - render_error_total: 错误统计(按引擎/任务类型/错误类型) - render_stream_copy_total: stream_copy命中率(hit/miss/fallback) - render_active_tasks: 活跃任务数(Gauge) 配套 9 个单元测试,覆盖成功/失败/超时/活跃数增减/stream_copy/错误分类等场景
196 lines
7.8 KiB
Python
Executable File
196 lines
7.8 KiB
Python
Executable File
"""渲染引擎灰度观测指标单元测试。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import time
|
|
|
|
import pytest
|
|
from prometheus_client import REGISTRY, CollectorRegistry, Counter, Gauge, Histogram
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_registry(monkeypatch):
|
|
"""每个测试使用独立的 CollectorRegistry,避免互相影响。"""
|
|
from video_processing import render_metrics
|
|
|
|
test_registry = CollectorRegistry()
|
|
|
|
# 用测试 registry 重新创建所有指标
|
|
task_total = Counter(
|
|
"render_task_total",
|
|
"Total number of render tasks",
|
|
["engine", "task_type", "status"],
|
|
registry=test_registry,
|
|
)
|
|
task_duration = Histogram(
|
|
"render_task_duration_seconds",
|
|
"Render task duration in seconds",
|
|
["engine", "task_type"],
|
|
buckets=(1, 5, 10, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200),
|
|
registry=test_registry,
|
|
)
|
|
error_total = Counter(
|
|
"render_error_total",
|
|
"Total number of render errors",
|
|
["engine", "task_type", "error_type"],
|
|
registry=test_registry,
|
|
)
|
|
stream_copy_total = Counter(
|
|
"render_stream_copy_total",
|
|
"Total number of stream copy attempts and results",
|
|
["result", "reason"],
|
|
registry=test_registry,
|
|
)
|
|
active_tasks = Gauge(
|
|
"render_active_tasks",
|
|
"Number of render tasks currently in progress",
|
|
["engine", "task_type"],
|
|
registry=test_registry,
|
|
)
|
|
|
|
# 替换模块中的指标对象
|
|
monkeypatch.setattr(render_metrics, "RENDER_TASK_TOTAL", task_total)
|
|
monkeypatch.setattr(render_metrics, "RENDER_TASK_DURATION_SECONDS", task_duration)
|
|
monkeypatch.setattr(render_metrics, "RENDER_ERROR_TOTAL", error_total)
|
|
monkeypatch.setattr(render_metrics, "RENDER_STREAM_COPY_TOTAL", stream_copy_total)
|
|
monkeypatch.setattr(render_metrics, "RENDER_ACTIVE_TASKS", active_tasks)
|
|
|
|
yield test_registry
|
|
|
|
|
|
def _counter_value(counter: Counter, **labels) -> float:
|
|
"""获取 Counter 指定标签的当前值。"""
|
|
return counter.labels(**labels)._value.get()
|
|
|
|
|
|
def _gauge_value(gauge: Gauge, **labels) -> float:
|
|
"""获取 Gauge 指定标签的当前值。"""
|
|
return gauge.labels(**labels)._value.get()
|
|
|
|
|
|
def _histogram_sum(histogram: Histogram, **labels) -> float:
|
|
"""获取 Histogram 指定标签的观测总和。"""
|
|
return histogram.labels(**labels)._sum.get()
|
|
|
|
|
|
def test_render_task_metrics_success():
|
|
"""成功任务应记录 success 状态 + 耗时 + 活跃数正确增减。"""
|
|
from video_processing.render_metrics import (
|
|
RENDER_ACTIVE_TASKS,
|
|
RENDER_TASK_DURATION_SECONDS,
|
|
RENDER_TASK_TOTAL,
|
|
render_task_metrics,
|
|
)
|
|
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="success") == 0
|
|
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="unified", task_type="compose_video") == 0
|
|
|
|
with render_task_metrics(engine="unified", task_type="compose_video"):
|
|
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="unified", task_type="compose_video") == 1
|
|
time.sleep(0.01)
|
|
|
|
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="unified", task_type="compose_video") == 0
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="success") == 1
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="failure") == 0
|
|
assert _histogram_sum(RENDER_TASK_DURATION_SECONDS, engine="unified", task_type="compose_video") > 0
|
|
|
|
|
|
def test_render_task_metrics_failure():
|
|
"""失败任务应记录 failure 状态 + 对应错误类型。"""
|
|
from video_processing.render_metrics import (
|
|
RENDER_ERROR_TOTAL,
|
|
RENDER_TASK_TOTAL,
|
|
render_task_metrics,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="test error"):
|
|
with render_task_metrics(engine="legacy", task_type="edit_plan"):
|
|
raise ValueError("test error")
|
|
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="legacy", task_type="edit_plan", status="failure") == 1
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="legacy", task_type="edit_plan", status="success") == 0
|
|
assert _counter_value(RENDER_ERROR_TOTAL, engine="legacy", task_type="edit_plan", error_type="validation") == 1
|
|
|
|
|
|
def test_render_task_metrics_ffmpeg_error():
|
|
"""FFmpeg 执行失败应分类为 ffmpeg_error。"""
|
|
from video_processing.render_metrics import RENDER_ERROR_TOTAL, render_task_metrics
|
|
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
with render_task_metrics(engine="unified", task_type="generate_video"):
|
|
raise subprocess.CalledProcessError(returncode=1, cmd=["ffmpeg"])
|
|
|
|
assert _counter_value(RENDER_ERROR_TOTAL, engine="unified", task_type="generate_video", error_type="ffmpeg_error") == 1
|
|
|
|
|
|
def test_render_task_metrics_timeout():
|
|
"""超时异常应分类为 timeout。"""
|
|
from video_processing.render_metrics import RENDER_ERROR_TOTAL, render_task_metrics
|
|
|
|
with pytest.raises(subprocess.TimeoutExpired):
|
|
with render_task_metrics(engine="unified", task_type="compose_video"):
|
|
raise subprocess.TimeoutExpired(cmd=["ffmpeg"], timeout=10)
|
|
|
|
assert _counter_value(RENDER_ERROR_TOTAL, engine="unified", task_type="compose_video", error_type="timeout") == 1
|
|
|
|
|
|
def test_render_task_metrics_active_tasks_decrement_on_error():
|
|
"""任务异常退出时,活跃任务数也应正确递减。"""
|
|
from video_processing.render_metrics import RENDER_ACTIVE_TASKS, render_task_metrics
|
|
|
|
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="legacy", task_type="compose_video") == 0
|
|
|
|
with pytest.raises(RuntimeError):
|
|
with render_task_metrics(engine="legacy", task_type="compose_video"):
|
|
raise RuntimeError("boom")
|
|
|
|
assert _gauge_value(RENDER_ACTIVE_TASKS, engine="legacy", task_type="compose_video") == 0
|
|
|
|
|
|
def test_record_stream_copy():
|
|
"""stream_copy 命中/跳过/回退应分别计数。"""
|
|
from video_processing.render_metrics import RENDER_STREAM_COPY_TOTAL, record_stream_copy
|
|
|
|
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="hit", reason="成功") == 0
|
|
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="miss", reason="编码不匹配") == 0
|
|
|
|
record_stream_copy("hit", "成功")
|
|
record_stream_copy("hit", "成功")
|
|
record_stream_copy("miss", "编码不匹配")
|
|
record_stream_copy("fallback", "ffmpeg失败")
|
|
|
|
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="hit", reason="成功") == 2
|
|
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="miss", reason="编码不匹配") == 1
|
|
assert _counter_value(RENDER_STREAM_COPY_TOTAL, result="fallback", reason="ffmpeg失败") == 1
|
|
|
|
|
|
def test_classify_error_unknown():
|
|
"""未知异常应分类为 unknown。"""
|
|
from video_processing.render_metrics import _classify_error
|
|
|
|
assert _classify_error(RuntimeError("something")) == "unknown"
|
|
|
|
|
|
def test_classify_error_io():
|
|
"""IO 异常应分类为 io_error。"""
|
|
from video_processing.render_metrics import _classify_error
|
|
|
|
assert _classify_error(OSError("disk full")) == "io_error"
|
|
|
|
|
|
def test_engine_and_task_type_labels():
|
|
"""不同 engine 和 task_type 应独立计数。"""
|
|
from video_processing.render_metrics import RENDER_TASK_TOTAL, render_task_metrics
|
|
|
|
with render_task_metrics(engine="unified", task_type="compose_video"):
|
|
pass
|
|
with render_task_metrics(engine="legacy", task_type="compose_video"):
|
|
pass
|
|
with render_task_metrics(engine="unified", task_type="edit_plan"):
|
|
pass
|
|
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="compose_video", status="success") == 1
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="legacy", task_type="compose_video", status="success") == 1
|
|
assert _counter_value(RENDER_TASK_TOTAL, engine="unified", task_type="edit_plan", status="success") == 1
|