4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
214 lines
6.2 KiB
Python
214 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
import requests
|
|
from dotenv import find_dotenv, load_dotenv
|
|
|
|
# Load environment from .env once (does NOT override already-set env vars)
|
|
load_dotenv(find_dotenv(usecwd=True), override=False)
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Debug switch:
|
|
# export TRACER_INGEST_DEBUG=1
|
|
# ---------------------------------------------------------------------
|
|
_DEBUG = os.getenv("TRACER_INGEST_DEBUG", "").strip().lower() in {"1", "true", "yes"}
|
|
|
|
|
|
def _log(msg: str) -> None:
|
|
"""Lightweight stdout logger for local debugging."""
|
|
if _DEBUG:
|
|
print(f"[tracer_ingest] {msg}")
|
|
|
|
|
|
def _redact_token(token: str) -> str:
|
|
"""Redact token to avoid leaking secrets in logs."""
|
|
if not token:
|
|
return "<empty>"
|
|
if len(token) <= 8:
|
|
return "<redacted>"
|
|
return f"{token[:4]}...{token[-4:]}"
|
|
|
|
|
|
def _utc_now_iso() -> str:
|
|
"""UTC timestamp in ISO-8601 with milliseconds and Z suffix."""
|
|
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
|
|
|
|
def _tracer_context_metadata() -> dict[str, Any]:
|
|
"""
|
|
Optional context from Tracer CLI / CI.
|
|
Attach to each event to make joins & debugging easier.
|
|
"""
|
|
ctx: dict[str, Any] = {}
|
|
|
|
tracer_run_id = (os.getenv("TRACER_RUN_ID") or "").strip()
|
|
tracer_trace_id = (os.getenv("TRACER_TRACE_ID") or "").strip()
|
|
tracer_pipeline = (os.getenv("TRACER_PIPELINE_NAME") or "").strip()
|
|
tracer_org = (os.getenv("TRACER_ORG_SLUG") or "").strip()
|
|
|
|
if tracer_run_id:
|
|
ctx["tracer_run_id"] = tracer_run_id
|
|
if tracer_trace_id:
|
|
ctx["tracer_trace_id"] = tracer_trace_id
|
|
if tracer_pipeline:
|
|
ctx["tracer_pipeline_name"] = tracer_pipeline
|
|
if tracer_org:
|
|
ctx["tracer_org_slug"] = tracer_org
|
|
|
|
# Optional GitHub Actions context (helps when debugging CI)
|
|
for k in (
|
|
"GITHUB_RUN_ID",
|
|
"GITHUB_RUN_ATTEMPT",
|
|
"GITHUB_WORKFLOW",
|
|
"GITHUB_JOB",
|
|
"GITHUB_REPOSITORY",
|
|
"GITHUB_SHA",
|
|
):
|
|
v = (os.getenv(k) or "").strip()
|
|
if v:
|
|
ctx[k.lower()] = v
|
|
|
|
return ctx
|
|
|
|
|
|
def emit_tool_event(
|
|
*,
|
|
trace_id: str,
|
|
run_id: str,
|
|
run_name: str,
|
|
tool_id: str,
|
|
tool_name: str,
|
|
tool_cmd: str = "synthetic",
|
|
start_time: str | None = None,
|
|
end_time: str | None = None,
|
|
exit_code: int = 0,
|
|
metadata: dict[str, Any] | None = None,
|
|
timeout_s: float = 5.0,
|
|
) -> bool:
|
|
"""
|
|
Best-effort event emitter to Tracer ingest endpoint.
|
|
Returns True if sent successfully, False otherwise.
|
|
|
|
Notes:
|
|
- "metadata" is merged with optional Tracer/CI context so the backend can
|
|
join/debug even if some ids are off during early integration.
|
|
"""
|
|
base_url = os.getenv("TRACER_API_URL", "").strip().rstrip("/")
|
|
token = os.getenv("TRACER_INGEST_TOKEN", "").strip()
|
|
|
|
_log(
|
|
"env snapshot: "
|
|
f"TRACER_API_URL={base_url if base_url else '<missing>'} "
|
|
f"TRACER_INGEST_TOKEN={_redact_token(token) if token else '<missing>'}"
|
|
)
|
|
|
|
if not base_url or not token:
|
|
_log("skip: missing TRACER_API_URL and/or TRACER_INGEST_TOKEN")
|
|
return False
|
|
|
|
start = start_time or _utc_now_iso()
|
|
end = end_time or start
|
|
|
|
# Merge context metadata (context first, explicit metadata wins)
|
|
merged_meta = {**_tracer_context_metadata(), **(metadata or {})}
|
|
|
|
payload = {
|
|
"events": [
|
|
{
|
|
"trace_id": trace_id,
|
|
"run_id": run_id,
|
|
"span_id": str(uuid.uuid4()),
|
|
"run_name": run_name,
|
|
"tool_id": tool_id,
|
|
"tool_name": tool_name,
|
|
"tool_cmd": tool_cmd,
|
|
"start_time": start,
|
|
"end_time": end,
|
|
"exit_code": int(exit_code),
|
|
"metadata": merged_meta,
|
|
}
|
|
]
|
|
}
|
|
|
|
url = f"{base_url}/api/tools/ingest"
|
|
_log(f"POST {url} tool_id={tool_id} exit_code={exit_code} timeout_s={timeout_s}")
|
|
|
|
try:
|
|
resp = requests.post(
|
|
url,
|
|
json=payload,
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
timeout=timeout_s,
|
|
)
|
|
|
|
ok = 200 <= resp.status_code < 300
|
|
_log(f"response: status={resp.status_code} ok={ok}")
|
|
|
|
if not ok:
|
|
# Keep response small to avoid noise / accidental secrets
|
|
body_preview = (resp.text or "")[:500]
|
|
_log(f"response body (preview): {body_preview}")
|
|
|
|
return ok
|
|
|
|
except requests.RequestException as exc:
|
|
_log(f"request error: {type(exc).__name__}: {exc}")
|
|
return False
|
|
except Exception as exc:
|
|
_log(f"unexpected error: {type(exc).__name__}: {exc}")
|
|
return False
|
|
|
|
|
|
class StepTimer:
|
|
"""Helper to measure step duration and emit a single event on completion."""
|
|
|
|
def __init__(
|
|
self,
|
|
trace_id: str,
|
|
run_id: str,
|
|
run_name: str,
|
|
tool_id: str,
|
|
tool_name: str,
|
|
tool_cmd: str,
|
|
):
|
|
self.trace_id = trace_id
|
|
self.run_id = run_id
|
|
self.run_name = run_name
|
|
self.tool_id = tool_id
|
|
self.tool_name = tool_name
|
|
self.tool_cmd = tool_cmd
|
|
self.start_iso = _utc_now_iso()
|
|
self.start_monotonic = time.monotonic()
|
|
|
|
_log(f"StepTimer start: tool_id={tool_id} start={self.start_iso}")
|
|
|
|
def finish(self, *, exit_code: int, metadata: dict[str, Any] | None = None) -> bool:
|
|
end_iso = _utc_now_iso()
|
|
runtime_ms = int((time.monotonic() - self.start_monotonic) * 1000)
|
|
|
|
meta = dict(metadata or {})
|
|
meta["runtime_ms"] = runtime_ms
|
|
|
|
_log(
|
|
f"StepTimer finish: tool_id={self.tool_id} "
|
|
f"runtime_ms={runtime_ms} exit_code={exit_code}"
|
|
)
|
|
|
|
return emit_tool_event(
|
|
trace_id=self.trace_id,
|
|
run_id=self.run_id,
|
|
run_name=self.run_name,
|
|
tool_id=self.tool_id,
|
|
tool_name=self.tool_name,
|
|
tool_cmd=self.tool_cmd,
|
|
start_time=self.start_iso,
|
|
end_time=end_iso,
|
|
exit_code=exit_code,
|
|
metadata=meta,
|
|
)
|