mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
feat: wire agent runtime metrics
This commit is contained in:
@@ -94,6 +94,36 @@ def test_initialize_llm_uses_chain_event_selection(monkeypatch) -> None:
|
||||
assert agent._llm_provider_selection["selected_provider_id"] == "provider-1"
|
||||
|
||||
|
||||
def test_record_usage_emits_low_cardinality_token_metrics() -> None:
|
||||
"""Token 指标只暴露归一供应商类别与输入输出方向。"""
|
||||
agent = MoviePilotAgent(session_id="usage-metrics", user_id="user-1")
|
||||
agent._llm_provider_selection = {"provider": "private-provider-name"}
|
||||
|
||||
with patch("app.agent.orchestrator.record_metric") as record_metric:
|
||||
agent._record_usage(
|
||||
{
|
||||
"has_usage": True,
|
||||
"input_usage_available": True,
|
||||
"input_tokens": 120,
|
||||
"output_tokens": 30,
|
||||
"total_tokens": 150,
|
||||
}
|
||||
)
|
||||
|
||||
record_metric.assert_any_call(
|
||||
"agent.token_usage",
|
||||
120,
|
||||
provider_type="custom",
|
||||
direction="input",
|
||||
)
|
||||
record_metric.assert_any_call(
|
||||
"agent.token_usage",
|
||||
30,
|
||||
provider_type="custom",
|
||||
direction="output",
|
||||
)
|
||||
|
||||
|
||||
def test_execute_agent_broadcasts_usage_on_success() -> None:
|
||||
"""Agent 执行成功后应广播聚合 token 用量事件。"""
|
||||
agent = MoviePilotAgent(session_id="usage-success", user_id="user-1")
|
||||
|
||||
@@ -14,6 +14,7 @@ Connection 与 aiosqlite 的线程都绑定在创建它的循环上。因此池
|
||||
"""
|
||||
import asyncio
|
||||
import threading
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -163,8 +164,16 @@ def test_fallback_slot_times_out_when_exhausted(monkeypatch):
|
||||
|
||||
async def run():
|
||||
db_module._fallback_slots.acquire() # 占满唯一名额
|
||||
with pytest.raises(TimeoutError):
|
||||
await db_module._acquire_fallback_slot()
|
||||
with patch("app.db.session.record_metric") as record_metric:
|
||||
with pytest.raises(TimeoutError):
|
||||
await db_module._acquire_fallback_slot()
|
||||
record_metric.assert_any_call("db.pool.timeout", backend="sqlite")
|
||||
record_metric.assert_any_call(
|
||||
"db.pool.wait",
|
||||
pytest.approx(0.05, abs=0.03),
|
||||
backend="sqlite",
|
||||
outcome="timeout",
|
||||
)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ from typing import Mapping
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import PlainTextResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
from app.adapters.observability import otel
|
||||
from app.adapters.web.metrics import HttpMetricsMiddleware
|
||||
from app.db.engine import _register_database_pool_metrics
|
||||
from app.runtime.extensions.plugin.lifecycle import observe_plugin_lifecycle
|
||||
from app.schemas.plugin import PluginRuntimeStatus
|
||||
from app.runtime.observability import (
|
||||
METRIC_SPECS,
|
||||
MetricSpec,
|
||||
@@ -137,3 +141,36 @@ def test_optional_otel_adapter_falls_back_to_noop(monkeypatch) -> None:
|
||||
monkeypatch.setattr(otel.importlib, "import_module", missing)
|
||||
|
||||
assert isinstance(otel.build_observation_port(), NoopObservationPort)
|
||||
|
||||
|
||||
def test_database_pool_checkout_updates_gauge() -> None:
|
||||
"""真实 SQLAlchemy checkout/checkin 应成对维护连接借出量。"""
|
||||
port = RecordingObservationPort()
|
||||
configure_observation(port)
|
||||
engine = create_engine("sqlite://")
|
||||
_register_database_pool_metrics(engine)
|
||||
|
||||
with engine.connect():
|
||||
pass
|
||||
engine.dispose()
|
||||
|
||||
records = [record for record in port.records if record[0].name == "db.pool.checked_out"]
|
||||
assert [record[1] for record in records] == [1.0, -1.0]
|
||||
assert all(record[2] == {"backend": "sqlite"} for record in records)
|
||||
|
||||
|
||||
def test_plugin_lifecycle_failed_status_records_error_outcome() -> None:
|
||||
"""被插件生命周期内部收敛的加载失败仍应记录 error。"""
|
||||
port = RecordingObservationPort()
|
||||
configure_observation(port)
|
||||
|
||||
@observe_plugin_lifecycle("start")
|
||||
def load_plugin() -> dict[str, PluginRuntimeStatus]:
|
||||
"""返回插件加载失败状态。"""
|
||||
return {"Example": PluginRuntimeStatus.LOAD_FAILED}
|
||||
|
||||
load_plugin()
|
||||
|
||||
spec, _, labels = port.records[-1]
|
||||
assert spec.name == "plugin.lifecycle.duration"
|
||||
assert labels == {"operation": "start", "outcome": "error"}
|
||||
|
||||
Reference in New Issue
Block a user