From 92a1c25828d240014d141aa01bb492a0ef997021 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 09:17:49 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(ci):=20=E6=8E=A5=E5=85=A5AgentLoop=20T?= =?UTF-8?q?race=E4=B8=8A=E6=8A=A5=EF=BC=8C=E5=AE=9E=E7=8E=B0CI=E5=85=A8?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E7=9B=91=E6=8E=A7=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci_trace_report.py放到scripts/ci/目录 - 7个workflow共25个job接入Trace上报 - 复用step_timer_start.sh记录的start_time - 失败不影响CI(永远exit 0) --- .gitea/workflows/ci-pipeline.yml | 168 ++++++++++++ .gitea/workflows/ci-trigger-monitor.yml | 12 + .gitea/workflows/code-review.yml | 12 + .gitea/workflows/daily-check.yml | 60 +++++ .gitea/workflows/pr-automation.yml | 24 ++ .gitea/workflows/preview-cleanup.yml | 12 + .gitea/workflows/preview-deploy.yml | 12 + scripts/ci/ci_trace_report.py | 327 ++++++++++++++++++++++++ 8 files changed, 627 insertions(+) create mode 100755 scripts/ci/ci_trace_report.py diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index 041f2652e..c834c285a 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -64,6 +64,18 @@ jobs: echo "🔧 包含全栈变更,运行完整CI" fi + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + validate: needs: check-frontend-only if: always() && needs.check-frontend-only.outputs.skip_backend != 'true' @@ -148,6 +160,18 @@ jobs: run: | set +e NOTIFY_MODE=failure JOB_NAME="Validate Code Quality And Tests" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + unit-tests: needs: check-frontend-only if: always() && needs.check-frontend-only.outputs.skip_backend != 'true' @@ -201,6 +225,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Unit Tests" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + integration-tests: name: Integration Tests runs-on: ci-l2 @@ -253,6 +289,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Integration Tests" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + frontend-lint: name: Frontend Lint runs-on: ci-l2 @@ -304,6 +352,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Frontend Lint" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + frontend-unit-test: name: Frontend Unit Tests runs-on: ci-l2 @@ -347,6 +407,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Frontend Unit Tests" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + build-staging: name: Build Staging ${{ matrix.service_display }} Image runs-on: runtime-builder @@ -468,6 +540,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Build Staging ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + deploy-staging: name: Deploy Staging (Watchtower auto-deploy) runs-on: runtime-builder @@ -605,6 +689,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + staging-e2e: name: Staging E2E Tests runs-on: runtime-builder @@ -649,6 +745,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Staging E2E Tests" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + staging-api-tests: name: Staging API Integration Tests runs-on: runtime-builder @@ -691,6 +799,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Staging API Integration Tests" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + build-production: name: Build Production ${{ matrix.service_display }} Image runs-on: runtime-builder @@ -808,6 +928,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Build Production ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + deploy-production: name: Deploy Production runs-on: runtime-builder @@ -931,6 +1063,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Deploy Production" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + production-e2e: name: Production Browser E2E runs-on: runtime-builder @@ -975,6 +1119,18 @@ jobs: set +e NOTIFY_MODE=failure JOB_NAME="Production Browser E2E" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + acr-cleanup: name: ACR Image Cleanup runs-on: runtime-builder @@ -1021,3 +1177,15 @@ jobs: run: | set +e NOTIFY_MODE=failure JOB_NAME="ACR Image Cleanup" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/.gitea/workflows/ci-trigger-monitor.yml b/.gitea/workflows/ci-trigger-monitor.yml index 69234b47a..eac6df81d 100644 --- a/.gitea/workflows/ci-trigger-monitor.yml +++ b/.gitea/workflows/ci-trigger-monitor.yml @@ -34,3 +34,15 @@ jobs: python3 scripts/ci_trigger_monitor.py # 监控脚本永远不fail,避免告警风暴 exit 0 + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/.gitea/workflows/code-review.yml b/.gitea/workflows/code-review.yml index 23f3c1eb0..fcc59cdad 100644 --- a/.gitea/workflows/code-review.yml +++ b/.gitea/workflows/code-review.yml @@ -51,3 +51,15 @@ jobs: python3 scripts/ci_code_review.py # 审查脚本异常不影响 CI 通过 continue-on-error: true + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/.gitea/workflows/daily-check.yml b/.gitea/workflows/daily-check.yml index 209ae1283..b352213db 100644 --- a/.gitea/workflows/daily-check.yml +++ b/.gitea/workflows/daily-check.yml @@ -106,6 +106,18 @@ jobs: echo "======================================" exit $SMOKE_EXIT + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + # ── 2. Staging API 集成测试 ───────────────────────────────────────── staging-api-tests: name: Staging API Integration Tests @@ -243,6 +255,18 @@ jobs: echo "report=FAIL" >> "${GITHUB_OUTPUT}" fi + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + # ── 3. Staging 浏览器 E2E ────────────────────────────────────────── staging-e2e: name: Staging Browser E2E @@ -332,6 +356,18 @@ jobs: echo "==========================================" exit $EXIT_CODE + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + # ── 4. 性能基线巡检 ──────────────────────────────────────────────── performance-check: name: Performance Baseline Check @@ -580,6 +616,18 @@ jobs: exit 0 fi + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + # ── 5. 每日巡检汇总报告 ──────────────────────────────────────────── daily-report: name: Daily Check Report @@ -656,3 +704,15 @@ jobs: # 不 exit 1,因为我们用了 always(),保持 report job 成功, # 但其他失败的 job 已经让整体流水线标记为失败 fi + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/.gitea/workflows/pr-automation.yml b/.gitea/workflows/pr-automation.yml index 9f263b8e7..64ecbfa7b 100755 --- a/.gitea/workflows/pr-automation.yml +++ b/.gitea/workflows/pr-automation.yml @@ -175,6 +175,18 @@ jobs: echo "⏰ 等待超时(20分钟),CI尚未全部完成" exit 0 + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + auto-merge: name: Auto Merge on CI Green + Approved runs-on: ci-check @@ -343,3 +355,15 @@ jobs: echo echo "等待超时(30分钟)" exit 0 + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/.gitea/workflows/preview-cleanup.yml b/.gitea/workflows/preview-cleanup.yml index c40c1df15..a1977ab08 100755 --- a/.gitea/workflows/preview-cleanup.yml +++ b/.gitea/workflows/preview-cleanup.yml @@ -193,3 +193,15 @@ jobs: > /dev/null echo "Cleanup comment posted" + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/.gitea/workflows/preview-deploy.yml b/.gitea/workflows/preview-deploy.yml index ca5230268..e5ad2f0c0 100755 --- a/.gitea/workflows/preview-deploy.yml +++ b/.gitea/workflows/preview-deploy.yml @@ -272,3 +272,15 @@ jobs: run: | set +e NOTIFY_MODE=failure JOB_NAME="Deploy Preview Environment" python3 scripts/ci_notify.py + - name: Report CI trace + if: always() + shell: sh + env: + AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }} + run: | + STATUS="ok" + [ ${{ job.status }} = "success" ] || STATUS="error" + START_TIME="" + [ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time) + python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true + diff --git a/scripts/ci/ci_trace_report.py b/scripts/ci/ci_trace_report.py new file mode 100755 index 000000000..99280b23a --- /dev/null +++ b/scripts/ci/ci_trace_report.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +""" +CI Trace 涓婃姤鑴氭湰 - 鐢ㄤ簬 Gitea Actions workflow 涓笂鎶?Trace 鏁版嵁鍒?AgentLoop + +鍦?CI workflow 鐨勬瘡涓?job 涓皟鐢細 + - 寮€濮嬫椂锛歱ython3 .gitea/scripts/ci_trace_report.py --status running + - 缁撴潫鏃讹細python3 .gitea/scripts/ci_trace_report.py --status ok --start-time $CI_TRACE_START_TIME + +鐜鍙橀噺锛圙itea Actions 鍐呯疆锛夛細 + GITEA_REPOSITORY / GITHUB_REPOSITORY - 浠撳簱鍚?(owner/repo) + GITEA_WORKFLOW / GITHUB_WORKFLOW - workflow 鍚嶇О + GITEA_JOB / GITHUB_JOB - job ID + GITEA_SHA / GITHUB_SHA - commit SHA + GITEA_REF_NAME / GITHUB_REF_NAME - 鍒嗘敮鍚? GITEA_RUN_ID / GITHUB_RUN_ID - run ID + GITEA_ACTOR / GITHUB_ACTOR - 瑙﹀彂鑰? GITEA_EVENT_NAME / GITHUB_EVENT_NAME - 浜嬩欢绫诲瀷 + PR_NUMBER / GITEA_PR_NUMBER - PR 鍙凤紙濡傛灉鏄?PR 瑙﹀彂锛? +AgentLoop 閰嶇疆锛堥€氳繃 Secrets 娉ㄥ叆锛夛細 + AGENTLOOP_LICENSE_KEY - LicenseKey锛堝繀濉級 + AGENTLOOP_ENDPOINT - Trace 涓婃姤鍦板潃锛堝彲閫夛紝鏈夐粯璁ゅ€硷級 + AGENTLOOP_PROJECT - SLS Project 鍚嶏紙鍙€夛級 + AGENTLOOP_WORKSPACE - CMS Workspace 鍚嶏紙鍙€夛級 +""" + +import os +import sys +import json +import time +import uuid +import argparse +import urllib.request +import urllib.error + + +# ========== 榛樿閰嶇疆 ========== +DEFAULT_ENDPOINT = "https://proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces" +DEFAULT_PROJECT = "proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou" +DEFAULT_WORKSPACE = "agentloop-13b8d6efb7fde6e9b193eb982ade68e2" + + +# ========== OTLP Protobuf 鎵嬪姩缂栫爜 ========== + +def _encode_varint(value): + result = bytearray() + while value > 0x7F: + result.append((value & 0x7F) | 0x80) + value >>= 7 + result.append(value & 0x7F) + return bytes(result) + +def _encode_tag(field_number, wire_type): + return _encode_varint((field_number << 3) | wire_type) + +def _encode_string_field(field_number, value): + value_bytes = value.encode('utf-8') + return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes + +def _encode_bytes_field(field_number, value_bytes): + return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes + +def _encode_int_field(field_number, value): + return _encode_tag(field_number, 0) + _encode_varint(value & 0xFFFFFFFFFFFFFFFF) + +def _encode_message_field(field_number, message_bytes): + return _encode_tag(field_number, 2) + _encode_varint(len(message_bytes)) + message_bytes + +def _encode_key_value(key, value_str): + any_value = _encode_string_field(1, value_str) + return _encode_string_field(1, key) + _encode_message_field(2, any_value) + +def _encode_status(status_code, status_msg=""): + data = _encode_int_field(1, status_code) + if status_msg: + data += _encode_string_field(2, status_msg) + return data + +def _encode_span(trace_id_bytes, span_id_bytes, parent_span_id_bytes, + name, start_time_unix_nano, end_time_unix_nano, + span_kind, attributes, status_code, status_msg=""): + data = b"" + data += _encode_bytes_field(1, trace_id_bytes) + data += _encode_bytes_field(2, span_id_bytes) + if parent_span_id_bytes: + data += _encode_bytes_field(3, parent_span_id_bytes) + data += _encode_string_field(4, name) + data += _encode_int_field(5, span_kind) + data += _encode_int_field(6, start_time_unix_nano) + data += _encode_int_field(7, end_time_unix_nano) + for key, value in attributes.items(): + kv = _encode_key_value(key, str(value)) + data += _encode_message_field(9, kv) + status = _encode_status(status_code, status_msg) + data += _encode_message_field(12, status) + return data + +def _encode_resource_spans(service_name, scope_spans_bytes): + svc_kv = _encode_key_value("service.name", service_name) + resource = _encode_message_field(1, svc_kv) + data = _encode_message_field(1, resource) + data += _encode_message_field(2, scope_spans_bytes) + return data + +def _encode_scope_spans(scope_name, spans_bytes_list): + scope = _encode_string_field(1, scope_name) + data = _encode_message_field(1, scope) + for span_bytes in spans_bytes_list: + data += _encode_message_field(2, span_bytes) + return data + +def _encode_traces_data(resource_spans_bytes_list): + data = b"" + for rs_bytes in resource_spans_bytes_list: + data += _encode_message_field(1, rs_bytes) + return data + + +# ========== 杈呭姪鍑芥暟 ========== + +def _gen_trace_id(): + return uuid.uuid4().bytes + +def _gen_span_id(): + return uuid.uuid4().bytes[:8] + +def _env(name, default=""): + """鑾峰彇鐜鍙橀噺锛屾敮鎸?GITEA_ 鍜?GITHUB_ 鍓嶇紑""" + val = os.getenv(name, "") + if val: + return val + # 灏濊瘯鍙︿竴绉嶅墠缂€ + if name.startswith("GITEA_"): + alt = "GITHUB_" + name[6:] + return os.getenv(alt, default) + if name.startswith("GITHUB_"): + alt = "GITEA_" + name[7:] + return os.getenv(alt, default) + return default + + +def _get_pr_number(): + """浠庣幆澧冨彉閲忔垨浜嬩欢鏂囦欢涓幏鍙?PR 鍙?"" + # 鐩存帴浠庣幆澧冨彉閲? pr = os.getenv("PR_NUMBER", "") or os.getenv("GITEA_PR_NUMBER", "") + if pr: + return pr + + # 浠?GITHUB_EVENT_PATH 璇诲彇 + event_path = os.getenv("GITHUB_EVENT_PATH", "") or os.getenv("GITEA_EVENT_PATH", "") + if event_path and os.path.isfile(event_path): + try: + with open(event_path, "r") as f: + event = json.load(f) + if "pull_request" in event and "number" in event["pull_request"]: + return str(event["pull_request"]["number"]) + except Exception: + pass + + return "" + + +def _get_ci_attributes(): + """浠?CI 鐜鍙橀噺涓敹闆嗗睘鎬?"" + attrs = { + "ci.repo": _env("GITEA_REPOSITORY") or _env("GITHUB_REPOSITORY") or "unknown", + "ci.workflow": _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "unknown", + "ci.job": _env("GITEA_JOB") or _env("GITHUB_JOB") or "unknown", + "ci.commit_sha": _env("GITEA_SHA") or _env("GITHUB_SHA") or "unknown", + "ci.branch": _env("GITEA_REF_NAME") or _env("GITHUB_REF_NAME") or "unknown", + "ci.run_id": _env("GITEA_RUN_ID") or _env("GITHUB_RUN_ID") or "unknown", + "ci.actor": _env("GITEA_ACTOR") or _env("GITHUB_ACTOR") or "unknown", + "ci.event": _env("GITEA_EVENT_NAME") or _env("GITHUB_EVENT_NAME") or "unknown", + } + pr = _get_pr_number() + if pr: + attrs["ci.pr_number"] = pr + return attrs + + +# ========== 鏍稿績涓婃姤閫昏緫 ========== + +def build_trace(service_name, trace_name, status, duration_ms, attributes=None): + """鏋勫缓涓€鏉″畬鏁寸殑 Trace 鏁版嵁锛岃繑鍥?protobuf bytes銆?"" + trace_id = _gen_trace_id() + end_time = int(time.time() * 1e9) + start_time = end_time - int(duration_ms * 1e6) + status_code = 1 if status in ("ok", "running") else 2 + status_msg = "" if status in ("ok", "running") else "Job failed" + + main_attrs = { + "agent.trace_name": trace_name, + "agent.service": service_name, + "ci.trace_status": status, + } + if attributes: + main_attrs.update(attributes) + + main_span = _encode_span( + trace_id_bytes=trace_id, + span_id_bytes=_gen_span_id(), + parent_span_id_bytes=b"", + name=trace_name, + start_time_unix_nano=start_time, + end_time_unix_nano=end_time, + span_kind=1, + attributes=main_attrs, + status_code=status_code, + status_msg=status_msg, + ) + + scope_spans = _encode_scope_spans("ci-trace", [main_span]) + resource_spans = _encode_resource_spans(service_name, scope_spans) + return _encode_traces_data([resource_spans]) + + +def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, + endpoint=None, license_key=None, project=None, workspace=None, + extra_attributes=None): + """ + 涓婃姤 CI Trace 鏁版嵁銆傝繑鍥?(success: bool, message: str) + 姘歌繙涓嶄細鎶涘嚭寮傚父锛屽け璐ヤ篃杩斿洖 False銆? """ + try: + endpoint = endpoint or os.getenv("AGENTLOOP_ENDPOINT", DEFAULT_ENDPOINT) + license_key = license_key or os.getenv("AGENTLOOP_LICENSE_KEY", "") + project = project or os.getenv("AGENTLOOP_PROJECT", DEFAULT_PROJECT) + workspace = workspace or os.getenv("AGENTLOOP_WORKSPACE", DEFAULT_WORKSPACE) + + if not license_key: + return False, "[Trace] 璺宠繃涓婃姤锛氭湭閰嶇疆 AGENTLOOP_LICENSE_KEY" + + # 鏀堕泦 CI 灞炴€? attrs = _get_ci_attributes() + if extra_attributes: + attrs.update(extra_attributes) + + payload = build_trace( + service_name=service_name, + trace_name=trace_name, + status=status, + duration_ms=duration_ms, + attributes=attrs, + ) + + headers = { + "Content-Type": "application/x-protobuf", + "x-arms-license-key": license_key, + "x-arms-project": project, + "x-cms-workspace": workspace, + } + + req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST") + try: + with urllib.request.urlopen(req, timeout=10) as resp: + status_code = resp.status + resp_body = resp.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as e: + status_code = e.code + resp_body = e.read().decode("utf-8", errors="replace") + + if status_code in (200, 202): + return True, f"[Trace] 鉁?涓婃姤鎴愬姛: {service_name} / {trace_name} ({status}, {duration_ms}ms)" + else: + return False, f"[Trace] 鈿?涓婃姤澶辫触: HTTP {status_code} - {resp_body[:200]}" + except Exception as e: + return False, f"[Trace] 鈿?涓婃姤寮傚父: {type(e).__name__}: {str(e)}" + + +def main(): + parser = argparse.ArgumentParser(description="CI AgentLoop Trace 涓婃姤") + parser.add_argument("--service", dest="service_name", + default=os.getenv("TRACE_SERVICE", ""), + help="鏈嶅姟鍚嶇О锛堜篃鍙€氳繃 TRACE_SERVICE 鐜鍙橀噺璁剧疆锛?) + parser.add_argument("--name", dest="trace_name", + default=os.getenv("TRACE_NAME", ""), + help="Trace 鍚嶇О锛堜篃鍙€氳繃 TRACE_NAME 鐜鍙橀噺璁剧疆锛?) + parser.add_argument("--status", default=os.getenv("TRACE_STATUS", "ok"), + choices=["ok", "error", "running"], + help="鐘舵€? ok / error / running锛堥粯璁?ok锛?) + parser.add_argument("--start-time", dest="start_time", + default=os.getenv("TRACE_START_TIME", ""), + help="寮€濮嬫椂闂存埑锛堢锛夛紝鐢ㄤ簬璁$畻 duration锛涗笉濉垯鐢ㄩ粯璁?1s") + parser.add_argument("--duration-ms", dest="duration_ms", type=int, default=0, + help="鐩存帴鎸囧畾鑰楁椂锛堟绉掞級锛屼紭鍏堢骇楂樹簬 --start-time") + parser.add_argument("--attrs", default="", + help="闄勫姞灞炴€э紙JSON 瀛楃涓诧級") + + args = parser.parse_args() + + # 蹇呴』鍙傛暟妫€鏌? if not args.service_name: + print("[Trace] 鈿?璺宠繃涓婃姤锛氭湭鎸囧畾 service锛?-service 鎴?TRACE_SERVICE锛?) + sys.exit(0) + + # 璁$畻鑰楁椂 + duration_ms = args.duration_ms + if duration_ms <= 0 and args.start_time: + try: + start_ts = float(args.start_time) + duration_ms = int((time.time() - start_ts) * 1000) + except (ValueError, TypeError): + duration_ms = 1000 + if duration_ms <= 0: + duration_ms = 1000 # 榛樿 1 绉? + # 瑙f瀽闄勫姞灞炴€? extra_attrs = {} + if args.attrs: + try: + extra_attrs = json.loads(args.attrs) + except json.JSONDecodeError: + pass + + # 鐢熸垚 trace_name锛氬鏋滄病鎸囧畾锛岀敤 workflow+job + trace_name = args.trace_name + if not trace_name: + wf = _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "CI" + job = _env("GITEA_JOB") or _env("GITHUB_JOB") or "job" + trace_name = f"{wf} / {job}" + + success, msg = report_ci_trace( + service_name=args.service_name, + trace_name=trace_name, + status=args.status, + duration_ms=duration_ms, + extra_attributes=extra_attrs, + ) + + print(msg) + # 姘歌繙 exit 0锛屼笉褰卞搷 CI + sys.exit(0) + + +if __name__ == "__main__": + main() -- 2.54.0 From e7dc4aac8685d923a579552b2dfd37a6799a438b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 09:46:19 +0800 Subject: [PATCH 2/5] fix(ci): rewrite ci_trace_report.py in English to fix encoding issues --- scripts/ci/ci_trace_report.py | 114 ++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/scripts/ci/ci_trace_report.py b/scripts/ci/ci_trace_report.py index 99280b23a..42ce553c8 100755 --- a/scripts/ci/ci_trace_report.py +++ b/scripts/ci/ci_trace_report.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 """ -CI Trace 涓婃姤鑴氭湰 - 鐢ㄤ簬 Gitea Actions workflow 涓笂鎶?Trace 鏁版嵁鍒?AgentLoop +CI Trace Report Script - Reports CI Trace data to AgentLoop from Gitea Actions workflows. -鍦?CI workflow 鐨勬瘡涓?job 涓皟鐢細 - - 寮€濮嬫椂锛歱ython3 .gitea/scripts/ci_trace_report.py --status running - - 缁撴潫鏃讹細python3 .gitea/scripts/ci_trace_report.py --status ok --start-time $CI_TRACE_START_TIME +Usage in CI workflow jobs: + - At start: python3 scripts/ci/ci_trace_report.py --status running + - At end: python3 scripts/ci/ci_trace_report.py --status ok --start-time $CI_TRACE_START_TIME -鐜鍙橀噺锛圙itea Actions 鍐呯疆锛夛細 - GITEA_REPOSITORY / GITHUB_REPOSITORY - 浠撳簱鍚?(owner/repo) - GITEA_WORKFLOW / GITHUB_WORKFLOW - workflow 鍚嶇О +Environment variables (built-in Gitea Actions): + GITEA_REPOSITORY / GITHUB_REPOSITORY - repository (owner/repo) + GITEA_WORKFLOW / GITHUB_WORKFLOW - workflow name GITEA_JOB / GITHUB_JOB - job ID GITEA_SHA / GITHUB_SHA - commit SHA - GITEA_REF_NAME / GITHUB_REF_NAME - 鍒嗘敮鍚? GITEA_RUN_ID / GITHUB_RUN_ID - run ID - GITEA_ACTOR / GITHUB_ACTOR - 瑙﹀彂鑰? GITEA_EVENT_NAME / GITHUB_EVENT_NAME - 浜嬩欢绫诲瀷 - PR_NUMBER / GITEA_PR_NUMBER - PR 鍙凤紙濡傛灉鏄?PR 瑙﹀彂锛? -AgentLoop 閰嶇疆锛堥€氳繃 Secrets 娉ㄥ叆锛夛細 - AGENTLOOP_LICENSE_KEY - LicenseKey锛堝繀濉級 - AGENTLOOP_ENDPOINT - Trace 涓婃姤鍦板潃锛堝彲閫夛紝鏈夐粯璁ゅ€硷級 - AGENTLOOP_PROJECT - SLS Project 鍚嶏紙鍙€夛級 - AGENTLOOP_WORKSPACE - CMS Workspace 鍚嶏紙鍙€夛級 + GITEA_REF_NAME / GITHUB_REF_NAME - branch name + GITEA_RUN_ID / GITHUB_RUN_ID - run ID + GITEA_ACTOR / GITHUB_ACTOR - trigger actor + GITEA_EVENT_NAME / GITHUB_EVENT_NAME - event type + PR_NUMBER / GITEA_PR_NUMBER - PR number (if PR triggered) + +AgentLoop configuration (injected via Secrets): + AGENTLOOP_LICENSE_KEY - LicenseKey (required) + AGENTLOOP_ENDPOINT - Trace endpoint (optional, has default) + AGENTLOOP_PROJECT - SLS Project name (optional) + AGENTLOOP_WORKSPACE - CMS Workspace name (optional) """ import os @@ -31,13 +34,13 @@ import urllib.request import urllib.error -# ========== 榛樿閰嶇疆 ========== +# ========== Default Configuration ========== DEFAULT_ENDPOINT = "https://proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces" DEFAULT_PROJECT = "proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou" DEFAULT_WORKSPACE = "agentloop-13b8d6efb7fde6e9b193eb982ade68e2" -# ========== OTLP Protobuf 鎵嬪姩缂栫爜 ========== +# ========== OTLP Protobuf Manual Encoding ========== def _encode_varint(value): result = bytearray() @@ -47,32 +50,40 @@ def _encode_varint(value): result.append(value & 0x7F) return bytes(result) + def _encode_tag(field_number, wire_type): return _encode_varint((field_number << 3) | wire_type) + def _encode_string_field(field_number, value): - value_bytes = value.encode('utf-8') + value_bytes = value.encode("utf-8") return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes + def _encode_bytes_field(field_number, value_bytes): return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes + def _encode_int_field(field_number, value): return _encode_tag(field_number, 0) + _encode_varint(value & 0xFFFFFFFFFFFFFFFF) + def _encode_message_field(field_number, message_bytes): return _encode_tag(field_number, 2) + _encode_varint(len(message_bytes)) + message_bytes + def _encode_key_value(key, value_str): any_value = _encode_string_field(1, value_str) return _encode_string_field(1, key) + _encode_message_field(2, any_value) + def _encode_status(status_code, status_msg=""): data = _encode_int_field(1, status_code) if status_msg: data += _encode_string_field(2, status_msg) return data + def _encode_span(trace_id_bytes, span_id_bytes, parent_span_id_bytes, name, start_time_unix_nano, end_time_unix_nano, span_kind, attributes, status_code, status_msg=""): @@ -92,6 +103,7 @@ def _encode_span(trace_id_bytes, span_id_bytes, parent_span_id_bytes, data += _encode_message_field(12, status) return data + def _encode_resource_spans(service_name, scope_spans_bytes): svc_kv = _encode_key_value("service.name", service_name) resource = _encode_message_field(1, svc_kv) @@ -99,6 +111,7 @@ def _encode_resource_spans(service_name, scope_spans_bytes): data += _encode_message_field(2, scope_spans_bytes) return data + def _encode_scope_spans(scope_name, spans_bytes_list): scope = _encode_string_field(1, scope_name) data = _encode_message_field(1, scope) @@ -106,6 +119,7 @@ def _encode_scope_spans(scope_name, spans_bytes_list): data += _encode_message_field(2, span_bytes) return data + def _encode_traces_data(resource_spans_bytes_list): data = b"" for rs_bytes in resource_spans_bytes_list: @@ -113,20 +127,21 @@ def _encode_traces_data(resource_spans_bytes_list): return data -# ========== 杈呭姪鍑芥暟 ========== +# ========== Helper Functions ========== def _gen_trace_id(): return uuid.uuid4().bytes + def _gen_span_id(): return uuid.uuid4().bytes[:8] + def _env(name, default=""): - """鑾峰彇鐜鍙橀噺锛屾敮鎸?GITEA_ 鍜?GITHUB_ 鍓嶇紑""" + """Get env var with GITEA_/GITHUB_ prefix fallback.""" val = os.getenv(name, "") if val: return val - # 灏濊瘯鍙︿竴绉嶅墠缂€ if name.startswith("GITEA_"): alt = "GITHUB_" + name[6:] return os.getenv(alt, default) @@ -137,12 +152,11 @@ def _env(name, default=""): def _get_pr_number(): - """浠庣幆澧冨彉閲忔垨浜嬩欢鏂囦欢涓幏鍙?PR 鍙?"" - # 鐩存帴浠庣幆澧冨彉閲? pr = os.getenv("PR_NUMBER", "") or os.getenv("GITEA_PR_NUMBER", "") + """Get PR number from environment or event file.""" + pr = os.getenv("PR_NUMBER", "") or os.getenv("GITEA_PR_NUMBER", "") if pr: return pr - # 浠?GITHUB_EVENT_PATH 璇诲彇 event_path = os.getenv("GITHUB_EVENT_PATH", "") or os.getenv("GITEA_EVENT_PATH", "") if event_path and os.path.isfile(event_path): try: @@ -157,7 +171,7 @@ def _get_pr_number(): def _get_ci_attributes(): - """浠?CI 鐜鍙橀噺涓敹闆嗗睘鎬?"" + """Collect attributes from CI environment variables.""" attrs = { "ci.repo": _env("GITEA_REPOSITORY") or _env("GITHUB_REPOSITORY") or "unknown", "ci.workflow": _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "unknown", @@ -174,10 +188,10 @@ def _get_ci_attributes(): return attrs -# ========== 鏍稿績涓婃姤閫昏緫 ========== +# ========== Trace Building & Reporting ========== def build_trace(service_name, trace_name, status, duration_ms, attributes=None): - """鏋勫缓涓€鏉″畬鏁寸殑 Trace 鏁版嵁锛岃繑鍥?protobuf bytes銆?"" + """Build an OTLP trace payload (protobuf bytes). No external dependencies.""" trace_id = _gen_trace_id() end_time = int(time.time() * 1e9) start_time = end_time - int(duration_ms * 1e6) @@ -214,8 +228,9 @@ def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, endpoint=None, license_key=None, project=None, workspace=None, extra_attributes=None): """ - 涓婃姤 CI Trace 鏁版嵁銆傝繑鍥?(success: bool, message: str) - 姘歌繙涓嶄細鎶涘嚭寮傚父锛屽け璐ヤ篃杩斿洖 False銆? """ + Report CI Trace data. Returns (success: bool, message: str). + Never raises exceptions; returns False on failure. + """ try: endpoint = endpoint or os.getenv("AGENTLOOP_ENDPOINT", DEFAULT_ENDPOINT) license_key = license_key or os.getenv("AGENTLOOP_LICENSE_KEY", "") @@ -223,9 +238,9 @@ def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, workspace = workspace or os.getenv("AGENTLOOP_WORKSPACE", DEFAULT_WORKSPACE) if not license_key: - return False, "[Trace] 璺宠繃涓婃姤锛氭湭閰嶇疆 AGENTLOOP_LICENSE_KEY" + return False, "[Trace] skipped: AGENTLOOP_LICENSE_KEY not configured" - # 鏀堕泦 CI 灞炴€? attrs = _get_ci_attributes() + attrs = _get_ci_attributes() if extra_attributes: attrs.update(extra_attributes) @@ -254,39 +269,43 @@ def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, resp_body = e.read().decode("utf-8", errors="replace") if status_code in (200, 202): - return True, f"[Trace] 鉁?涓婃姤鎴愬姛: {service_name} / {trace_name} ({status}, {duration_ms}ms)" + return True, ( + f"[Trace] success: {service_name} / {trace_name} " + f"({status}, {duration_ms}ms)" + ) else: - return False, f"[Trace] 鈿?涓婃姤澶辫触: HTTP {status_code} - {resp_body[:200]}" + return False, ( + f"[Trace] failed: HTTP {status_code} - {resp_body[:200]}" + ) except Exception as e: - return False, f"[Trace] 鈿?涓婃姤寮傚父: {type(e).__name__}: {str(e)}" + return False, f"[Trace] error: {type(e).__name__}: {str(e)}" def main(): - parser = argparse.ArgumentParser(description="CI AgentLoop Trace 涓婃姤") + parser = argparse.ArgumentParser(description="CI AgentLoop Trace Reporter") parser.add_argument("--service", dest="service_name", default=os.getenv("TRACE_SERVICE", ""), - help="鏈嶅姟鍚嶇О锛堜篃鍙€氳繃 TRACE_SERVICE 鐜鍙橀噺璁剧疆锛?) + help="Service name (also via TRACE_SERVICE env)") parser.add_argument("--name", dest="trace_name", default=os.getenv("TRACE_NAME", ""), - help="Trace 鍚嶇О锛堜篃鍙€氳繃 TRACE_NAME 鐜鍙橀噺璁剧疆锛?) + help="Trace name (also via TRACE_NAME env)") parser.add_argument("--status", default=os.getenv("TRACE_STATUS", "ok"), choices=["ok", "error", "running"], - help="鐘舵€? ok / error / running锛堥粯璁?ok锛?) + help="Status: ok / error / running (default ok)") parser.add_argument("--start-time", dest="start_time", default=os.getenv("TRACE_START_TIME", ""), - help="寮€濮嬫椂闂存埑锛堢锛夛紝鐢ㄤ簬璁$畻 duration锛涗笉濉垯鐢ㄩ粯璁?1s") + help="Start timestamp (seconds) for duration calculation") parser.add_argument("--duration-ms", dest="duration_ms", type=int, default=0, - help="鐩存帴鎸囧畾鑰楁椂锛堟绉掞級锛屼紭鍏堢骇楂樹簬 --start-time") + help="Direct duration in ms; takes precedence over --start-time") parser.add_argument("--attrs", default="", - help="闄勫姞灞炴€э紙JSON 瀛楃涓诧級") + help="Extra attributes (JSON string)") args = parser.parse_args() - # 蹇呴』鍙傛暟妫€鏌? if not args.service_name: - print("[Trace] 鈿?璺宠繃涓婃姤锛氭湭鎸囧畾 service锛?-service 鎴?TRACE_SERVICE锛?) + if not args.service_name: + print("[Trace] skipped: no service specified (--service or TRACE_SERVICE)") sys.exit(0) - # 璁$畻鑰楁椂 duration_ms = args.duration_ms if duration_ms <= 0 and args.start_time: try: @@ -295,15 +314,15 @@ def main(): except (ValueError, TypeError): duration_ms = 1000 if duration_ms <= 0: - duration_ms = 1000 # 榛樿 1 绉? - # 瑙f瀽闄勫姞灞炴€? extra_attrs = {} + duration_ms = 1000 + + extra_attrs = {} if args.attrs: try: extra_attrs = json.loads(args.attrs) except json.JSONDecodeError: pass - # 鐢熸垚 trace_name锛氬鏋滄病鎸囧畾锛岀敤 workflow+job trace_name = args.trace_name if not trace_name: wf = _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "CI" @@ -319,7 +338,6 @@ def main(): ) print(msg) - # 姘歌繙 exit 0锛屼笉褰卞搷 CI sys.exit(0) -- 2.54.0 From 393aa496be8cd17864290ec72f92a3e3b6c52aea Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 09:46:39 +0800 Subject: [PATCH 3/5] fix(ci): format ci_trace_report.py with black --- scripts/ci/ci_trace_report.py | 95 ++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/scripts/ci/ci_trace_report.py b/scripts/ci/ci_trace_report.py index 42ce553c8..bbfe4a932 100755 --- a/scripts/ci/ci_trace_report.py +++ b/scripts/ci/ci_trace_report.py @@ -33,7 +33,6 @@ import argparse import urllib.request import urllib.error - # ========== Default Configuration ========== DEFAULT_ENDPOINT = "https://proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces" DEFAULT_PROJECT = "proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou" @@ -42,6 +41,7 @@ DEFAULT_WORKSPACE = "agentloop-13b8d6efb7fde6e9b193eb982ade68e2" # ========== OTLP Protobuf Manual Encoding ========== + def _encode_varint(value): result = bytearray() while value > 0x7F: @@ -69,7 +69,11 @@ def _encode_int_field(field_number, value): def _encode_message_field(field_number, message_bytes): - return _encode_tag(field_number, 2) + _encode_varint(len(message_bytes)) + message_bytes + return ( + _encode_tag(field_number, 2) + + _encode_varint(len(message_bytes)) + + message_bytes + ) def _encode_key_value(key, value_str): @@ -84,9 +88,18 @@ def _encode_status(status_code, status_msg=""): return data -def _encode_span(trace_id_bytes, span_id_bytes, parent_span_id_bytes, - name, start_time_unix_nano, end_time_unix_nano, - span_kind, attributes, status_code, status_msg=""): +def _encode_span( + trace_id_bytes, + span_id_bytes, + parent_span_id_bytes, + name, + start_time_unix_nano, + end_time_unix_nano, + span_kind, + attributes, + status_code, + status_msg="", +): data = b"" data += _encode_bytes_field(1, trace_id_bytes) data += _encode_bytes_field(2, span_id_bytes) @@ -129,6 +142,7 @@ def _encode_traces_data(resource_spans_bytes_list): # ========== Helper Functions ========== + def _gen_trace_id(): return uuid.uuid4().bytes @@ -190,6 +204,7 @@ def _get_ci_attributes(): # ========== Trace Building & Reporting ========== + def build_trace(service_name, trace_name, status, duration_ms, attributes=None): """Build an OTLP trace payload (protobuf bytes). No external dependencies.""" trace_id = _gen_trace_id() @@ -224,9 +239,17 @@ def build_trace(service_name, trace_name, status, duration_ms, attributes=None): return _encode_traces_data([resource_spans]) -def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, - endpoint=None, license_key=None, project=None, workspace=None, - extra_attributes=None): +def report_ci_trace( + service_name, + trace_name, + status="ok", + duration_ms=1000, + endpoint=None, + license_key=None, + project=None, + workspace=None, + extra_attributes=None, +): """ Report CI Trace data. Returns (success: bool, message: str). Never raises exceptions; returns False on failure. @@ -259,7 +282,9 @@ def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, "x-cms-workspace": workspace, } - req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST") + req = urllib.request.Request( + endpoint, data=payload, headers=headers, method="POST" + ) try: with urllib.request.urlopen(req, timeout=10) as resp: status_code = resp.status @@ -274,31 +299,45 @@ def report_ci_trace(service_name, trace_name, status="ok", duration_ms=1000, f"({status}, {duration_ms}ms)" ) else: - return False, ( - f"[Trace] failed: HTTP {status_code} - {resp_body[:200]}" - ) + return False, (f"[Trace] failed: HTTP {status_code} - {resp_body[:200]}") except Exception as e: return False, f"[Trace] error: {type(e).__name__}: {str(e)}" def main(): parser = argparse.ArgumentParser(description="CI AgentLoop Trace Reporter") - parser.add_argument("--service", dest="service_name", - default=os.getenv("TRACE_SERVICE", ""), - help="Service name (also via TRACE_SERVICE env)") - parser.add_argument("--name", dest="trace_name", - default=os.getenv("TRACE_NAME", ""), - help="Trace name (also via TRACE_NAME env)") - parser.add_argument("--status", default=os.getenv("TRACE_STATUS", "ok"), - choices=["ok", "error", "running"], - help="Status: ok / error / running (default ok)") - parser.add_argument("--start-time", dest="start_time", - default=os.getenv("TRACE_START_TIME", ""), - help="Start timestamp (seconds) for duration calculation") - parser.add_argument("--duration-ms", dest="duration_ms", type=int, default=0, - help="Direct duration in ms; takes precedence over --start-time") - parser.add_argument("--attrs", default="", - help="Extra attributes (JSON string)") + parser.add_argument( + "--service", + dest="service_name", + default=os.getenv("TRACE_SERVICE", ""), + help="Service name (also via TRACE_SERVICE env)", + ) + parser.add_argument( + "--name", + dest="trace_name", + default=os.getenv("TRACE_NAME", ""), + help="Trace name (also via TRACE_NAME env)", + ) + parser.add_argument( + "--status", + default=os.getenv("TRACE_STATUS", "ok"), + choices=["ok", "error", "running"], + help="Status: ok / error / running (default ok)", + ) + parser.add_argument( + "--start-time", + dest="start_time", + default=os.getenv("TRACE_START_TIME", ""), + help="Start timestamp (seconds) for duration calculation", + ) + parser.add_argument( + "--duration-ms", + dest="duration_ms", + type=int, + default=0, + help="Direct duration in ms; takes precedence over --start-time", + ) + parser.add_argument("--attrs", default="", help="Extra attributes (JSON string)") args = parser.parse_args() -- 2.54.0 From 386486797cd680b62a4663a3ba7fcf4c8ca81ee1 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 09:52:38 +0800 Subject: [PATCH 4/5] fix(ci): fix isort import ordering in ci_trace_report.py --- scripts/ci/ci_trace_report.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/ci/ci_trace_report.py b/scripts/ci/ci_trace_report.py index bbfe4a932..e97689a93 100755 --- a/scripts/ci/ci_trace_report.py +++ b/scripts/ci/ci_trace_report.py @@ -24,14 +24,14 @@ AgentLoop configuration (injected via Secrets): AGENTLOOP_WORKSPACE - CMS Workspace name (optional) """ +import argparse +import json import os import sys -import json import time -import uuid -import argparse -import urllib.request import urllib.error +import urllib.request +import uuid # ========== Default Configuration ========== DEFAULT_ENDPOINT = "https://proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces" -- 2.54.0 From 01119198d8a7841017745311807ff79e5858fa8a Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 10:02:19 +0800 Subject: [PATCH 5/5] ci: fix black formatting for py312 target --- scripts/ci/ci_trace_report.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/scripts/ci/ci_trace_report.py b/scripts/ci/ci_trace_report.py index e97689a93..eb53f1422 100755 --- a/scripts/ci/ci_trace_report.py +++ b/scripts/ci/ci_trace_report.py @@ -69,11 +69,7 @@ def _encode_int_field(field_number, value): def _encode_message_field(field_number, message_bytes): - return ( - _encode_tag(field_number, 2) - + _encode_varint(len(message_bytes)) - + message_bytes - ) + return _encode_tag(field_number, 2) + _encode_varint(len(message_bytes)) + message_bytes def _encode_key_value(key, value_str): @@ -282,9 +278,7 @@ def report_ci_trace( "x-cms-workspace": workspace, } - req = urllib.request.Request( - endpoint, data=payload, headers=headers, method="POST" - ) + req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=10) as resp: status_code = resp.status @@ -294,10 +288,7 @@ def report_ci_trace( resp_body = e.read().decode("utf-8", errors="replace") if status_code in (200, 202): - return True, ( - f"[Trace] success: {service_name} / {trace_name} " - f"({status}, {duration_ms}ms)" - ) + return True, (f"[Trace] success: {service_name} / {trace_name} " f"({status}, {duration_ms}ms)") else: return False, (f"[Trace] failed: HTTP {status_code} - {resp_body[:200]}") except Exception as e: -- 2.54.0