feat: track logical call statistics across governed calls
This commit is contained in:
+23
-5
@@ -40,12 +40,14 @@ from polygateway.middleware.retry import StallClock, _failure_reason, backoff_de
|
||||
from polygateway.middleware.telemetry import TelemetryEmitter
|
||||
from polygateway.ports import OutcomeAwareSelector
|
||||
from polygateway.types import (
|
||||
CallStats,
|
||||
ChatRequest,
|
||||
LLMResponse,
|
||||
OcrLayoutResult,
|
||||
OcrTextResult,
|
||||
TelemetryStatus,
|
||||
Usage,
|
||||
_CallContext,
|
||||
strip_unsupported_extra_body,
|
||||
validate_caller_dimensions,
|
||||
)
|
||||
@@ -176,7 +178,7 @@ class OcrClient:
|
||||
dimension_tenant_id, dimensions = validate_caller_dimensions(
|
||||
tenant_id, meta, origin="recognize_text(tenant_id=..., meta=...)"
|
||||
)
|
||||
outcome = await self._call(
|
||||
outcome, call_stats = await self._call(
|
||||
"text", image, session_id, parent_call_id, dimension_tenant_id, dimensions
|
||||
)
|
||||
result = outcome.result
|
||||
@@ -187,6 +189,7 @@ class OcrClient:
|
||||
latency_ms=outcome.latency_ms,
|
||||
call_id=outcome.call_id,
|
||||
raw=result.raw,
|
||||
call_stats=call_stats,
|
||||
)
|
||||
|
||||
async def parse_layout(
|
||||
@@ -206,7 +209,7 @@ class OcrClient:
|
||||
dimension_tenant_id, dimensions = validate_caller_dimensions(
|
||||
tenant_id, meta, origin="parse_layout(tenant_id=..., meta=...)"
|
||||
)
|
||||
outcome = await self._call(
|
||||
outcome, call_stats = await self._call(
|
||||
"layout", image, session_id, parent_call_id, dimension_tenant_id, dimensions
|
||||
)
|
||||
result = outcome.result
|
||||
@@ -218,6 +221,7 @@ class OcrClient:
|
||||
latency_ms=outcome.latency_ms,
|
||||
call_id=outcome.call_id,
|
||||
raw=result.raw,
|
||||
call_stats=call_stats,
|
||||
)
|
||||
|
||||
async def check_health(self) -> dict[str, bool]:
|
||||
@@ -242,11 +246,14 @@ class OcrClient:
|
||||
parent_call_id: str | None,
|
||||
tenant_id: str | None,
|
||||
meta: dict[str, Any],
|
||||
) -> _AttemptOutcome:
|
||||
) -> tuple[_AttemptOutcome, CallStats]:
|
||||
if not isinstance(image, bytes):
|
||||
raise TypeError("image 必须是 bytes(路径读取/批量拼帧留业务侧,D9)")
|
||||
if not image:
|
||||
raise ValueError("image 不能为空")
|
||||
# M1 例外: `image` 校验在 `_call` 内而非公开方法,故上下文在该校验
|
||||
# **通过之后**创建——这样设计 §3 的"校验在统计边界外"对 OCR 才成立
|
||||
context = _CallContext(now=self._now)
|
||||
if not self._sources:
|
||||
raise AllSourcesExhausted(scope=self._scope, reason="no_sources", retry_after_s=0.0)
|
||||
fails = 0
|
||||
@@ -260,10 +267,18 @@ class OcrClient:
|
||||
continue
|
||||
async with clock.attempting():
|
||||
outcome = await self._attempt(
|
||||
kind, image, *picked, reasons, session_id, parent_call_id, tenant_id, meta
|
||||
kind,
|
||||
image,
|
||||
*picked,
|
||||
reasons,
|
||||
session_id,
|
||||
parent_call_id,
|
||||
tenant_id,
|
||||
meta,
|
||||
context,
|
||||
)
|
||||
if isinstance(outcome, _AttemptOutcome):
|
||||
return outcome
|
||||
return outcome, context.snapshot()
|
||||
fails += 1
|
||||
if fails >= self._retry.max_attempts:
|
||||
raise AllSourcesExhausted(
|
||||
@@ -287,11 +302,14 @@ class OcrClient:
|
||||
parent_call_id: str | None,
|
||||
tenant_id: str | None,
|
||||
meta: dict[str, Any],
|
||||
context: _CallContext,
|
||||
) -> _AttemptOutcome | _FailedAttempt:
|
||||
call_id = str(uuid.uuid4())
|
||||
started = self._now()
|
||||
# 四个 emit 分支(成功/终态拒绝/取消/可重试失败)都必须带调用方维度:
|
||||
# 失败行与取消行同样需要租户归属,漏掉任一分支就会写出无归属的行
|
||||
# layout 的 POST + ZIP GET 在同一次 `_invoke` 内,故这里只登记 **1** 次
|
||||
context.register_attempt()
|
||||
try:
|
||||
result = await self._invoke(kind, image, source, call_id)
|
||||
await self._record_quietly(self._breaker.record_success(entry))
|
||||
|
||||
Reference in New Issue
Block a user