Files
wyndham-ARR/arr_web/job_trace.py

681 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Privacy-safe, structured task trace projection for the ARR portal."""
from __future__ import annotations
from datetime import date, datetime, timezone
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
ACTIVE_RUN_STATUSES = {"received", "queued", "running", "validating"}
_CURRENT_STAGE = {
"received": "upload",
"queued": "processor",
"running": "processor",
"validating": "validation",
"accepted": "database",
"rejected": "validation",
"failed": "processor",
"cancelled": "processor",
}
_CURRENT_COPY = {
"received": ("当前:文件已接收", "等待创建程序化处理任务。"),
"queued": ("当前:等待程序处理", "任务已进入 ARR 处理队列。"),
"running": ("当前ARR 正在处理", "固定处理器正在生成日报和完整结构化结果。"),
"validating": ("当前:正在独立验收", "ARR 正在复核文件身份、业务结果并准备数据库提交。"),
}
_LOG_ORDER = {
"UPLOAD_REGISTERED": 10,
"ATTEMPT_QUEUED": 20,
"PROCESSOR_STARTED": 30,
"ARTIFACT_RESULT_RECEIVED": 40,
"DIRECT_RESULT_RECEIVED": 40,
"ARTIFACT_RESULT_VALIDATED": 50,
"DIRECT_VALIDATION_STARTED": 50,
"FINANCE_VERSION_CREATED": 60,
"FINANCE_VERSION_VALIDATED": 70,
"FINANCE_VERSION_ACTIVATED": 80,
"ARTIFACT_RESULT_COMMITTED": 85,
"DIRECT_RESULT_COMMITTED": 85,
"DIRECT_RESULT_ALREADY_COMMITTED": 85,
"ATTEMPT_SUCCEEDED": 90,
"JOB_SUCCEEDED": 95,
"DOWNSTREAM_FAILURE_QUEUED": 100,
"DOWNSTREAM_EVENT_QUEUED": 100,
"DOWNSTREAM_EVENT_PUBLISHED": 110,
}
def _iso(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, (date, datetime)):
return value.isoformat()
return str(value)
def _safe_text(value: Any, maximum: int = 500) -> Optional[str]:
if value is None:
return None
normalized = " ".join(str(value).split())
return normalized[:maximum] if normalized else None
def _safe_details(values: Mapping[str, Any]) -> Dict[str, Any]:
output: Dict[str, Any] = {}
for key, value in values.items():
if value is None or value == "":
continue
if isinstance(value, (date, datetime)):
output[key] = value.isoformat()
elif isinstance(value, str):
output[key] = _safe_text(value, 256)
elif isinstance(value, (bool, int, float)):
output[key] = value
return output
def _timestamp_key(value: str) -> float:
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc).timestamp()
except (TypeError, ValueError):
return 0.0
def _failure_stage(code: Any) -> str:
normalized = str(code or "").upper()
if normalized in {"PROCESSING_RESULT_MISSING", "SUBMISSION_EXPIRED"}:
return "processor"
if any(token in normalized for token in ("SUBMISSION", "CALLBACK", "DELIVERY")):
return "validation"
if any(
token in normalized
for token in ("VALIDATION", "CONTRACT", "SCHEMA", "MISMATCH", "SIGNATURE")
):
return "validation"
if "DATABASE" in normalized:
return "database"
if any(token in normalized for token in ("UPLOAD", "OBJECT_STORE", "SOURCE")):
return "upload"
if any(token in normalized for token in ("DISPATCH", "TRANSPORT", "REMOTE", "AGENT", "PROCESSOR")):
return "processor"
return "processor"
def _execution_metadata(delivery_mode: Any) -> Dict[str, str]:
"""Describe ARR execution without exposing host or network location."""
normalized = str(delivery_mode or "").strip().lower()
if normalized == "artifact_callback":
return {
"execution_scope": "arr_runtime",
"processor_mode": "fixed_processor",
"remote_dispatch": "none",
}
if normalized == "direct_mcp":
return {
"execution_scope": "remote_agent",
"processor_mode": "legacy_direct_mcp",
"remote_dispatch": "mcp",
}
return {
"execution_scope": "unknown",
"processor_mode": "unknown",
"remote_dispatch": "unknown",
}
def _append_log(
logs: List[Dict[str, Any]],
*,
log_id: str,
timestamp: Any,
stage: str,
level: str,
code: str,
title: str,
message: str,
details: Optional[Mapping[str, Any]] = None,
) -> None:
resolved_timestamp = _iso(timestamp)
if not resolved_timestamp:
return
item: Dict[str, Any] = {
"id": log_id,
"timestamp": resolved_timestamp,
"stage": stage,
"level": level,
"code": code,
"title": title,
"message": _safe_text(message, 1_200) or "",
"_order": _LOG_ORDER.get(code, 85),
"_sequence": len(logs),
}
safe_details = _safe_details(details or {})
if safe_details:
item["details"] = safe_details
logs.append(item)
def _latest(values: Sequence[Mapping[str, Any]], key: str) -> Optional[Mapping[str, Any]]:
if not values:
return None
return max(values, key=lambda item: int(item.get(key) or 0))
def _attempt_logs(logs: List[Dict[str, Any]], attempts: Sequence[Mapping[str, Any]]) -> None:
for attempt in attempts:
number = int(attempt.get("attempt_no") or 0)
details = {
"attempt_no": number,
"attempt_status": attempt.get("attempt_status"),
"remote_run_id": attempt.get("remote_run_id"),
}
_append_log(
logs,
log_id=f"attempt.{number}.queued",
timestamp=attempt.get("created_at"),
stage="processor",
level="info",
code="ATTEMPT_QUEUED",
title=f"处理尝试 #{number} 已创建",
message="任务已登记到 ARR 程序化处理队列。",
details=details,
)
if attempt.get("started_at"):
_append_log(
logs,
log_id=f"attempt.{number}.started",
timestamp=attempt.get("started_at"),
stage="processor",
level="info",
code="PROCESSOR_STARTED",
title="固定处理器已启动",
message="ARR 已开始处理已登记的源 XML。",
details=details,
)
status = str(attempt.get("attempt_status") or "")
if status in {"failed", "cancelled"} and attempt.get("finished_at"):
failure_code = attempt.get("failure_code")
_append_log(
logs,
log_id=f"attempt.{number}.{status}",
timestamp=attempt.get("finished_at"),
stage=_failure_stage(failure_code),
level="error" if status == "failed" else "warning",
code=str(failure_code or f"ATTEMPT_{status.upper()}"),
title="程序化处理失败" if status == "failed" else "程序化处理已取消",
message=(
_safe_text(attempt.get("failure_message"))
or "程序化处理尝试未完成。"
),
details={**details, "failure_code": failure_code},
)
elif status == "succeeded" and attempt.get("finished_at"):
_append_log(
logs,
log_id=f"attempt.{number}.succeeded",
timestamp=attempt.get("finished_at"),
stage="database",
level="success",
code="ATTEMPT_SUCCEEDED",
title="处理尝试已闭环",
message="ARR 固定处理器输出已通过验收并完成业务提交。",
details=details,
)
def _delivery_logs(
logs: List[Dict[str, Any]], deliveries: Sequence[Mapping[str, Any]]
) -> None:
for index, delivery in enumerate(deliveries, 1):
identity = delivery.get("delivery_key") or index
details = {
"delivery_id": delivery.get("delivery_key"),
"attempt_no": delivery.get("attempt_no"),
"delivery_status": delivery.get("delivery_status"),
"result_status": delivery.get("result_status"),
"processor_version": delivery.get("processor_version"),
"business_date": delivery.get("business_date"),
"daily_version_id": delivery.get("daily_version_id"),
}
_append_log(
logs,
log_id=f"delivery.{identity}.received",
timestamp=delivery.get("received_at"),
stage="validation",
level="success",
code="ARTIFACT_RESULT_RECEIVED",
title="程序输出制品已登记",
message="ARR 已登记固定处理器输出制品并开始独立验收。",
details=details,
)
if delivery.get("validated_at"):
_append_log(
logs,
log_id=f"delivery.{identity}.validated",
timestamp=delivery.get("validated_at"),
stage="validation",
level="success",
code="ARTIFACT_RESULT_VALIDATED",
title="结果制品已通过验收",
message="处理器版本、规则、文件身份与业务结果已完成复核。",
details=details,
)
status = str(delivery.get("delivery_status") or "")
if delivery.get("committed_at"):
failed = status in {"recorded_failure", "rejected"}
failure_code = delivery.get("failure_code")
_append_log(
logs,
log_id=f"delivery.{identity}.finished",
timestamp=delivery.get("committed_at"),
stage="validation" if failed else "database",
level="error" if failed else "success",
code=str(
failure_code
or ("ARTIFACT_RESULT_REJECTED" if failed else "ARTIFACT_RESULT_COMMITTED")
),
title="结果制品未通过验收" if failed else "结果制品已提交数据库",
message=(
_safe_text(delivery.get("failure_message"))
if failed
else "验收结果已原子提交到 Finance 数据库。"
)
or "结果制品未通过验收。",
details={**details, "failure_code": failure_code},
)
def _submission_logs(
logs: List[Dict[str, Any]], submissions: Sequence[Mapping[str, Any]]
) -> None:
for index, submission in enumerate(submissions, 1):
identity = submission.get("submission_key") or index
details = {
"submission_id": submission.get("submission_key"),
"attempt_no": submission.get("attempt_no"),
"submission_status": submission.get("submission_status"),
"processor_version": submission.get("processor_version"),
"business_date": submission.get("business_date"),
"record_count": submission.get("record_count"),
"daily_version_id": submission.get("daily_version_id"),
}
_append_log(
logs,
log_id=f"submission.{identity}.received",
timestamp=submission.get("created_at"),
stage="validation",
level="success",
code="DIRECT_RESULT_RECEIVED",
title="兼容结构化结果已接收",
message="ARR 已接收历史兼容链路提交的结构化结果。",
details=details,
)
if submission.get("validation_started_at"):
_append_log(
logs,
log_id=f"submission.{identity}.validating",
timestamp=submission.get("validation_started_at"),
stage="validation",
level="info",
code="DIRECT_VALIDATION_STARTED",
title="独立结果验收已开始",
message="ARR 正在重放确定性规则并比对业务事实。",
details=details,
)
status = str(submission.get("submission_status") or "")
if submission.get("finished_at"):
succeeded = status in {"committed", "already_committed"}
failure_code = submission.get("failure_code")
_append_log(
logs,
log_id=f"submission.{identity}.finished",
timestamp=submission.get("finished_at"),
stage="database" if succeeded else _failure_stage(failure_code),
level="success" if succeeded else "error",
code=str(
"DIRECT_RESULT_ALREADY_COMMITTED"
if status == "already_committed"
else "DIRECT_RESULT_COMMITTED"
if succeeded
else failure_code or f"DIRECT_RESULT_{status.upper()}"
),
title=(
"相同结果已提交过"
if status == "already_committed"
else "结构化结果已提交数据库"
if succeeded
else "结构化结果未通过验收"
),
message=(
"已返回同一任务的既有数据库回执。"
if status == "already_committed"
else "验证通过的业务事实已原子提交到 Finance 数据库。"
if succeeded
else _safe_text(submission.get("failure_message"))
or "结构化结果未通过 ARR 独立验收。"
),
details={**details, "failure_code": failure_code},
)
def _version_logs(logs: List[Dict[str, Any]], versions: Sequence[Mapping[str, Any]]) -> None:
for version in versions:
identity = version.get("daily_version_id")
details = {
"daily_version_id": identity,
"business_date": version.get("business_date"),
"version_no": version.get("version_no"),
"version_status": version.get("version_status"),
"delivery_mode": version.get("result_delivery_mode"),
"source_rows": version.get("source_rows"),
"retained_rows": version.get("retained_rows"),
"excluded_rate_code_rows": version.get("excluded_rate_code_rows"),
"duplicate_rows": version.get("duplicate_rows"),
"validation_failed_rows": version.get("validation_failed_rows"),
"price_unmatched_rows": version.get("price_unmatched_rows"),
}
_append_log(
logs,
log_id=f"version.{identity}.created",
timestamp=version.get("created_at"),
stage="database",
level="info",
code="FINANCE_VERSION_CREATED",
title="Finance 日报版本已创建",
message="业务结果已形成可追溯的日报版本。",
details=details,
)
if version.get("validated_at"):
failed = str(version.get("version_status") or "") == "rejected"
_append_log(
logs,
log_id=f"version.{identity}.validated",
timestamp=version.get("validated_at"),
stage="validation" if failed else "database",
level="error" if failed else "success",
code=str(
version.get("failure_code")
or ("FINANCE_VERSION_REJECTED" if failed else "FINANCE_VERSION_VALIDATED")
),
title="Finance 版本已拒绝" if failed else "Finance 版本已验证",
message=(
_safe_text(version.get("failure_message"))
if failed
else "行数对账、定价与版本约束已通过。"
)
or "Finance 版本未通过验证。",
details={**details, "failure_code": version.get("failure_code")},
)
if version.get("activated_at"):
_append_log(
logs,
log_id=f"version.{identity}.activated",
timestamp=version.get("activated_at"),
stage="database",
level="success",
code="FINANCE_VERSION_ACTIVATED",
title="Finance 当前版本已激活",
message="当前业务日期已指向本次提交的日报事实。",
details=details,
)
def _outbox_logs(logs: List[Dict[str, Any]], events: Sequence[Mapping[str, Any]]) -> None:
for index, event in enumerate(events, 1):
identity = event.get("event_key") or index
event_type = str(event.get("event_type") or "")
is_failure = event_type == "arr.processing_failed"
details = {
"outbox_event": event_type,
"publish_status": event.get("publish_status"),
"publish_attempts": event.get("publish_attempts"),
"last_error_code": event.get("last_error_code"),
}
_append_log(
logs,
log_id=f"outbox.{identity}.created",
timestamp=event.get("created_at"),
stage="downstream",
level="warning" if is_failure else "info",
code="DOWNSTREAM_FAILURE_QUEUED" if is_failure else "DOWNSTREAM_EVENT_QUEUED",
title="失败通知已进入下游队列" if is_failure else "下游处理通知已入队",
message=(
"任务失败事实已写入可靠事件队列。"
if is_failure
else "Finance 数据库提交已完成;事件已写入可靠队列,等待下游消费者。"
),
details=details,
)
status = str(event.get("publish_status") or "")
if event.get("published_at"):
_append_log(
logs,
log_id=f"outbox.{identity}.published",
timestamp=event.get("published_at"),
stage="downstream",
level="success",
code="DOWNSTREAM_EVENT_PUBLISHED",
title="下游处理通知已发布",
message="事件已交付给下游消费边界;这不等同于报表已生成。",
details=details,
)
elif status == "dead":
_append_log(
logs,
log_id=f"outbox.{identity}.dead",
timestamp=event.get("available_at") or event.get("created_at"),
stage="downstream",
level="error",
code=str(event.get("last_error_code") or "DOWNSTREAM_EVENT_DEAD"),
title="下游处理通知投递失败",
message="可靠事件已进入 dead 状态,需要检查消费者或投递器。",
details=details,
)
elif int(event.get("publish_attempts") or 0) > 0 and event.get("last_error_code"):
_append_log(
logs,
log_id=f"outbox.{identity}.retrying",
timestamp=event.get("available_at") or event.get("created_at"),
stage="downstream",
level="warning",
code=str(event.get("last_error_code")),
title="下游处理通知正在重试",
message="最近一次投递未完成,事件仍保留在可靠队列中。",
details=details,
)
def build_job_trace(
run: Mapping[str, Any],
*,
attempts: Sequence[Mapping[str, Any]] = (),
deliveries: Sequence[Mapping[str, Any]] = (),
submissions: Sequence[Mapping[str, Any]] = (),
versions: Sequence[Mapping[str, Any]] = (),
outbox_events: Sequence[Mapping[str, Any]] = (),
refreshed_at: Optional[datetime] = None,
) -> Dict[str, Any]:
"""Build one complete trace using only explicitly allowlisted facts."""
logs: List[Dict[str, Any]] = []
execution = _execution_metadata(run.get("delivery_mode"))
_append_log(
logs,
log_id="run.received",
timestamp=run.get("created_at"),
stage="upload",
level="success",
code="UPLOAD_REGISTERED",
title="上传已安全登记",
message="XML 对象身份和任务身份已写入受控处理链路。",
details={
"job_id": run.get("job_id"),
"filename": run.get("filename"),
"source_byte_size": run.get("byte_size"),
"source_sha256": run.get("source_sha256"),
},
)
_attempt_logs(logs, attempts)
_delivery_logs(logs, deliveries)
_submission_logs(logs, submissions)
_version_logs(logs, versions)
_outbox_logs(logs, outbox_events)
run_status = str(run.get("run_status") or "")
public_status = str(run.get("public_status") or run_status)
failure_code = run.get("failure_code")
if run_status in {"failed", "rejected", "cancelled"} and run.get("finished_at"):
cancelled = run_status == "cancelled"
_append_log(
logs,
log_id=f"run.{run_status}",
timestamp=run.get("finished_at"),
stage=_failure_stage(failure_code),
level="warning" if cancelled else "error",
code=str(failure_code or f"JOB_{run_status.upper()}"),
title="任务已取消" if cancelled else "业务任务处理失败",
message=(
_safe_text(run.get("failure_message"))
or (
"任务在收到可提交结果前被取消。"
if cancelled
else "任务未形成可接受的数据库提交。"
)
),
details={
"job_id": run.get("job_id"),
"run_status": run_status,
"failure_code": failure_code,
"delivery_mode": run.get("delivery_mode"),
},
)
elif run_status == "accepted" and run.get("finished_at"):
_append_log(
logs,
log_id="run.accepted",
timestamp=run.get("finished_at"),
stage="database",
level="success",
code="JOB_SUCCEEDED",
title="业务任务已完成",
message="只有通过验收并提交 Finance 数据库后,任务才会进入此状态。",
details={
"job_id": run.get("job_id"),
"business_date": run.get("business_date"),
"delivery_mode": run.get("delivery_mode"),
},
)
elif run_status in ACTIVE_RUN_STATUSES:
title, message = _CURRENT_COPY[run_status]
_append_log(
logs,
log_id=f"run.current.{run_status}",
timestamp=run.get("updated_at") or run.get("created_at"),
stage=_CURRENT_STAGE[run_status],
level="info",
code=f"CURRENT_{run_status.upper()}",
title=title,
message=message,
details={
"job_id": run.get("job_id"),
"run_status": run_status,
"delivery_mode": run.get("delivery_mode"),
},
)
logs.sort(
key=lambda item: (
_timestamp_key(str(item["timestamp"])),
int(item["_order"]),
int(item["_sequence"]),
)
)
for item in logs:
item.pop("_order", None)
item.pop("_sequence", None)
latest_attempt = _latest(attempts, "attempt_no")
latest_version = _latest(versions, "version_no")
current_stage = (
_failure_stage(failure_code)
if failure_code
else _CURRENT_STAGE.get(run_status, logs[-1]["stage"] if logs else "upload")
)
if run_status == "accepted" and outbox_events:
current_stage = "downstream"
last_log_at = logs[-1]["timestamp"] if logs else _iso(run.get("updated_at"))
resolved_refreshed_at = refreshed_at or datetime.now(timezone.utc)
return {
"trace_version": "arr-job-trace-3",
"job": {
"job_id": str(run.get("job_id") or ""),
"status": public_status,
"raw_status": run_status,
"active": run_status in ACTIVE_RUN_STATUSES,
"current_stage": current_stage,
"filename": _safe_text(run.get("filename"), 255),
"byte_size": int(run.get("byte_size") or 0),
"source_sha256": _safe_text(run.get("source_sha256"), 64),
"business_date": _iso(run.get("business_date")),
"delivery_mode": _safe_text(run.get("delivery_mode"), 32),
"execution_scope": execution["execution_scope"],
"processor_mode": execution["processor_mode"],
"remote_dispatch": execution["remote_dispatch"],
"attempt_no": (
int(latest_attempt.get("attempt_no") or 0)
if latest_attempt is not None
else None
),
"remote_run_id": (
_safe_text(latest_attempt.get("remote_run_id"), 128)
if latest_attempt is not None
else None
),
"version_no": (
int(latest_version.get("version_no") or 0)
if latest_version is not None
and latest_version.get("version_no") is not None
else None
),
"output_rows": (
int(latest_version.get("retained_rows") or 0)
if latest_version is not None
else 0
),
"created_at": _iso(run.get("created_at")),
"updated_at": _iso(run.get("updated_at")),
"finished_at": _iso(run.get("finished_at")),
"last_log_at": last_log_at,
"failure": (
{
"code": _safe_text(failure_code, 128),
"message": _safe_text(run.get("failure_message")),
}
if failure_code
else None
),
"requested_processor_version": _safe_text(
run.get("requested_processor_version"), 64
),
"delivered_processor_version": _safe_text(
run.get("delivered_processor_version"), 64
),
},
"evidence": {
"attempts": len(attempts),
"artifact_deliveries": len(deliveries),
"legacy_direct_submissions": len(submissions),
"finance_versions": len(versions),
"outbox_events": len(outbox_events),
},
"logs": logs,
"refreshed_at": resolved_refreshed_at.isoformat(),
}