Files
Agentswarm/benchmark/export/__init__.py
T
gongzhiyongandClaude Sonnet 4.6 ffe9277050 feat(benchmark): 默认启用 Cosmos + Blob 导出
BENCHMARK_EXPORT_TARGET 默认值从 none 改为 cosmos,blob,
每次真实 run 终态自动落库。凭据未配置时降级 noop + warning,不影响主路径。
同步更新 ENV_VARS.md 默认值说明。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 00:20:26 +08:00

133 lines
6.0 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.
"""Benchmark metrics 导出器(阶段2/3 数据采集落库)。
把每次真实 run 终态采集到的 `SwarmMetrics` 快照导出到外部留存,供后续【经验标定】
(O / BASE_COEFFICIENTS / S_gain)累积真实用户数据。
目标可组合,由 `BENCHMARK_EXPORT_TARGET`(逗号分隔)选择:
- `none`(默认) → NoopExporter:不导出、无依赖、无凭据(指标仍落 run.metadata Redis)。
- `cosmos` → CosmosExporter:写 Cosmos DB for NoSQL(结构化指标,便于查询/排行榜)。
- `blob` → BlobExporter:写 Azure Blob 存储账户(全量快照归档)。
例:`BENCHMARK_EXPORT_TARGET=cosmos,blob`。
**凭据一律经环境/Secret 注入,绝不写进代码、日志或提交**(组织安全规则):
Cosmos: BENCHMARK_COSMOS_CONNECTION_STRING(必填)
BENCHMARK_COSMOS_DATABASE(默认 benchmark)/ BENCHMARK_COSMOS_CONTAINER(默认 selfcert)
Blob: AZURE_STORAGE_CONNECTION_STRING(或 BENCHMARK_BLOB_ACCOUNT_URL + Managed/Workload Identity)
BENCHMARK_BLOB_CONTAINER(默认 benchmark-selfcert)
"""
from __future__ import annotations
import json
import logging
import os
from typing import List, Optional
logger = logging.getLogger(__name__)
class MetricsExporter:
"""导出一次 run 的 benchmark 指标快照。子类实现 export()。构造不连网(懒连接)。"""
name = "abstract"
def export(self, swarm_id: str, payload: dict) -> None:
raise NotImplementedError
class NoopExporter(MetricsExporter):
"""默认:不导出(指标已落 run.metadata)。无外部依赖、无凭据。"""
name = "noop"
def export(self, swarm_id: str, payload: dict) -> None:
logger.debug("benchmark export disabled; run %s metrics persisted to run.metadata only", swarm_id)
class CosmosExporter(MetricsExporter):
"""Azure Cosmos DB for NoSQL(SQL API)。一 run 一文档,id=swarm_id,分区键 /swarm_id。"""
name = "cosmos"
def __init__(self, connection_string: str, database: str, container: str):
if not connection_string:
raise RuntimeError("CosmosExporter 需 BENCHMARK_COSMOS_CONNECTION_STRING")
self.connection_string = connection_string
self.database = database
self.container = container
@staticmethod
def build_doc(payload: dict) -> dict:
"""Cosmos 文档:需要字符串 id;分区键字段 swarm_id 已在 payload 内。"""
return {"id": str(payload.get("swarm_id")), **payload}
def _container_client(self):
from azure.cosmos import CosmosClient # lazy: keep core import-light & key-free
client = CosmosClient.from_connection_string(self.connection_string)
db = client.get_database_client(self.database)
return db.get_container_client(self.container)
def export(self, swarm_id: str, payload: dict) -> None:
self._container_client().upsert_item(self.build_doc(payload))
logger.info("benchmark metrics → cosmos %s/%s (id=%s)", self.database, self.container, swarm_id)
class BlobExporter(MetricsExporter):
"""Azure Blob 存储账户(懒加载 SDK)。归档路径 <scenario>/<swarm_id>.json。"""
name = "blob"
def __init__(self, container: str, *, connection_string: Optional[str] = None,
account_url: Optional[str] = None):
if not (connection_string or account_url):
raise RuntimeError("BlobExporter 需 AZURE_STORAGE_CONNECTION_STRING 或 BENCHMARK_BLOB_ACCOUNT_URL")
self.container = container
self.connection_string = connection_string
self.account_url = account_url
def _client(self):
from azure.storage.blob import BlobServiceClient # lazy
if self.connection_string:
return BlobServiceClient.from_connection_string(self.connection_string)
from azure.identity import DefaultAzureCredential # Workload/Managed Identity, no secret
return BlobServiceClient(account_url=self.account_url, credential=DefaultAzureCredential())
def export(self, swarm_id: str, payload: dict) -> None:
scenario = payload.get("scenario") or "unknown"
blob_name = f"{scenario}/{swarm_id}.json"
client = self._client().get_blob_client(container=self.container, blob=blob_name)
client.upload_blob(json.dumps(payload, ensure_ascii=False), overwrite=True)
logger.info("benchmark metrics → blob %s/%s", self.container, blob_name)
def _build_target(target: str) -> Optional[MetricsExporter]:
"""Build one exporter from a target name; None (with a warning) if misconfigured/unknown."""
if target in ("none", "off", ""):
return NoopExporter()
try:
if target == "cosmos":
return CosmosExporter(
connection_string=os.getenv("BENCHMARK_COSMOS_CONNECTION_STRING", ""),
database=os.getenv("BENCHMARK_COSMOS_DATABASE", "benchmark"),
container=os.getenv("BENCHMARK_COSMOS_CONTAINER", "selfcert"),
)
if target == "blob":
return BlobExporter(
container=os.getenv("BENCHMARK_BLOB_CONTAINER", "benchmark-selfcert"),
connection_string=os.getenv("AZURE_STORAGE_CONNECTION_STRING"),
account_url=os.getenv("BENCHMARK_BLOB_ACCOUNT_URL"),
)
except Exception as exc:
# Misconfigured target (e.g. missing creds) must not break the others or the run.
logger.warning("benchmark export target '%s' disabled: %s", target, exc)
return None
logger.warning("unknown BENCHMARK_EXPORT_TARGET entry '%s'; ignored", target)
return None
def get_exporters() -> List[MetricsExporter]:
"""Parse BENCHMARK_EXPORT_TARGET (comma list) → exporters. Default → cosmos,blob."""
raw = (os.getenv("BENCHMARK_EXPORT_TARGET") or "cosmos,blob").strip().lower()
targets = [t.strip() for t in raw.split(",") if t.strip()] or ["none"]
exporters = [e for e in (_build_target(t) for t in targets) if e is not None]
return exporters or [NoopExporter()]