From 53d941e523209543b9005dd57ea05a59e17877be Mon Sep 17 00:00:00 2001 From: z00930736 Date: Mon, 12 Oct 2026 11:40:57 +0800 Subject: [PATCH 1/7] docs(sample): bilingual README for observability-sample Rewrite the observability-sample README as a pair of docs (README.md in English, README-zh.md in Chinese) in the same friendly style as the container README: business-first scenario walkthroughs (native / a2at / negotiation with the real per-round timing), a quick start that works copy-paste, an optional Langfuse section, a plain-language cheat sheet of what gets recorded, and a compact environment-variable table. The deep technical details (span names, handler caveat, verification helpers) move to a short Notes section. Also register the sample in the container READMEs (directory tree + use-case table). Signed-off-by: z00930736 --- a2a-t-sample/README-zh.md | 1 + a2a-t-sample/README.md | 1 + .../observability-sample/README-zh.md | 132 +++++++++++++ a2a-t-sample/observability-sample/README.md | 179 +++++++++++------- 4 files changed, 243 insertions(+), 70 deletions(-) create mode 100644 a2a-t-sample/observability-sample/README-zh.md diff --git a/a2a-t-sample/README-zh.md b/a2a-t-sample/README-zh.md index 64db6c35..ac108e42 100644 --- a/a2a-t-sample/README-zh.md +++ b/a2a-t-sample/README-zh.md @@ -15,3 +15,4 @@ | [fault-management/](fault-management/) | 事件订阅样例——流式推送 Incident 通知 | | [ran-energy-saving/](ran-energy-saving/) | 无线网络节能样例——运行 RAN 节能任务 | | [negotiation/](negotiation/) | 协商闭环样例——离线 propose → accept 往返 | +| [observability-sample/](observability-sample/) | 可观测样例——所有场景的 trace / 指标 / 日志,可选 Langfuse 后端 | diff --git a/a2a-t-sample/README.md b/a2a-t-sample/README.md index 70fe5e19..55964539 100644 --- a/a2a-t-sample/README.md +++ b/a2a-t-sample/README.md @@ -15,3 +15,4 @@ See the repository root [`README.md`](../README.md) for setup and how to run. | [fault-management/](fault-management/) | Event subscription sample — stream Incident notifications | | [ran-energy-saving/](ran-energy-saving/) | RAN energy saving sample — run a RAN energy-saving task | | [negotiation/](negotiation/) | Negotiation closed-loop sample — offline propose → accept round trips | +| [observability-sample/](observability-sample/) | Observability sample — traces, metrics and logs for every scenario, with an optional Langfuse backend | diff --git a/a2a-t-sample/observability-sample/README-zh.md b/a2a-t-sample/observability-sample/README-zh.md new file mode 100644 index 00000000..10e2f6be --- /dev/null +++ b/a2a-t-sample/observability-sample/README-zh.md @@ -0,0 +1,132 @@ +# observability-sample — 看见你的 A2A-T 智能体在做什么 + +一个可直接运行的样例:展示 **A2A-T 交互的完整生命周期**——客户端发起请求、服务端处理、 +结果流式返回——每一步都由 `a2a-t-sdk` 的可观测层自动记录(trace / 指标 / 日志)。 + +样例里**没有任何手写的 OpenTelemetry 代码**:服务端加一个包装器、客户端做一次注册, +其余全部由 SDK 自动完成。 + +--- + +## 可以体验什么 + +| 场景(`--scenario=`) | 故事 | +|------------------------|------| +| `native` | 一个任务完整走完:客户端提交、服务端处理后流式返回 artifact。观察请求如何把两端"点亮"。 | +| `a2at` | 用 LLM 生成任务 prompt(默认 mock LLM——无需 API Key),并在 SDK 调用外手动加一个"L4" Span。 | +| `negotiation` | 智能体**先谈价、后干活**:两轮报价通过纯消息完成(此时还没有任务),双方达成一致后,真正的任务才启动并流式返回结果。 | + +## 快速开始 + +前置条件:已在仓库根目录,且安装了 [uv](https://docs.astral.sh/uv/)。 + +```bash +# 仓库根目录——终端 1:启动服务端(端口 8100) +cd a2a-t-sample +export PYTHONPATH="$PWD/observability-sample/src" +export A2AT_OBSERVABILITY_ENABLED=true +uv run python -m obs_sample.server_main + +# 终端 2:运行客户端场景 +cd a2a-t-sample +export PYTHONPATH="$PWD/observability-sample/src" +uv run python -m obs_sample.client_main --scenario=native +``` + +Windows PowerShell:把 `export` 换成 `$env:PYTHONPATH = "$PWD\observability-sample\src"`。 + +两个终端都会输出日志(`[server] ...` / `[client] ...`);默认配置下服务端还会把每个 +span 以 JSON 打印到控制台。 + +## 场景说明 + +### native — 一个任务,两端可见 + +1. 终端 1 启动服务端;终端 2 以 `--scenario=native` 运行客户端。 +2. 客户端提交任务;服务端经历 *submitted → working* 并流式返回 artifact。 +3. 每一步在**两端**都以 span 呈现,并汇入同一条 trace(客户端自动把 W3C `traceparent` + 传给服务端)。 + +### a2at — 用 LLM 生成 prompt + +```bash +uv run python -m obs_sample.client_main --scenario=a2at +``` + +客户端通过 SDK 的 LLM 门面生成任务 prompt。`a2a-t-sample/.env` 里 `A2AT_LLM_API_KEY` +留空时会使用**mock LLM**(固定回复),完全离线可跑。LLM 的 token 用量会被自动计量并 +挂到 trace 上。 + +### negotiation — 先谈成,再执行 + +```bash +# 终端 1:以协商模式重启服务端 +uv run python -m obs_sample.server_main --scenario=negotiation +# 终端 2: +uv run python -m obs_sample.client_main --scenario=negotiation +``` + +这是 Negotiation-T 谈判的真实时序: + +1. **第 1 轮** — 客户端出价;服务端还一个不同的价并断开连接。*此时还没有任务—— + 谈判通过纯消息进行。* +2. **第 2 轮** — 客户端重连并接受对方的还价;服务端确认达成一致,再次断开。 +3. **达成一致后任务才启动** — 客户端以独立请求启动真正的任务生命周期并接收结果。 + +每一轮都是一次独立交互;样例把它们串起来,让你能在同一个地方看到整场谈判 +(见下方 Langfuse)。 + +## 把所有数据发到 Langfuse(可选) + +在启动**两个进程之前**设置三个变量,所有 span 就会进入你的 Langfuse 项目—— +每次交互一条可浏览的 trace: + +```bash +export LANGFUSE_PUBLIC_KEY="pk-lf-..." +export LANGFUSE_SECRET_KEY="sk-lf-..." +export LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" +``` + +在 Langfuse 界面里,每一轮的客户端/服务端 span 和最终任务会呈现在同一条时间线上—— +谁出了什么价、哪一轮被接受、每一步耗时多久,一目了然。 + +## 记录了什么(速查表) + +| 你做的事 | SDK 记录的内容 | +|----------|----------------| +| 客户端发请求 | `SendStreamingMessage` span(CLIENT)+ 耗时指标 | +| 服务端收到请求 | `SendStreamingMessage` span(SERVER),是客户端 span 的子节点 | +| 服务端流式发出事件 | `SendStreamingMessage-event` span,LINK 关联入口 | +| 发出 / 收到协商消息 | `SendStreamingMessage-negotiation` span(含轮次与施为动词) | +| 服务端推送通知 | `SendMessage-pushNotification` span,LINK 关联服务端入口 | +| LLM 消耗 token | token 用量指标 + 下一跳 trace 上的 `gen_ai.usage.*` | +| 值得记录的事件 | logger `a2at.observability` 上的结构化日志 | + +三类信号可独立开关——见下方环境变量。 + +## 环境变量 + +常用: + +| 变量 | 默认值 | 含义 | +|------|--------|------| +| `A2AT_OBSERVABILITY_ENABLED` | `true` | 总开关——`false` 时 SDK 完全透明 | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | 未设置 | 把 trace/指标/日志发到 OTLP collector,而不是控制台 | +| `A2AT_CONSOLE_EXPORTER` | `true` | `false` 关闭 dev 模式控制台兜底(stdout 不再输出 JSON) | +| `OTEL_SERVICE_NAME` | `a2a-t-agent` | 本进程在后端的名称 | +| `A2AT_OBS_SAMPLE_PORT` | `8100` | 样例服务端端口 | +| `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` | 未设置 | 启用 Langfuse 后端 | + +进阶(较少用到):`A2AT_TRACE_ENABLED`、`A2AT_METRIC_ENABLED`、`A2AT_LOG_ENABLED` +(按信号独立开关——例如关 trace 后指标与日志仍工作)、`A2AT_EXPORTER_PROTOCOL` +(`grpc` / `http/protobuf`)、`OTEL_INSTRUMENTATION_A2AT_SDK_ENABLED=false`(硬关闭)。 + +## 说明 + +- 服务端有意使用 a2a-sdk 的 `LegacyRequestHandler`:新版 `DefaultRequestHandler` + (a2a-sdk 1.1.x V2)在构造期就捕获了 executor / push-sender 的原始引用,会绕过 SDK + 的包装器。 +- 推送通知会回环到服务端的 `/push-sink`,因此样例能端到端展示 + `SendMessage-pushNotification` span。 +- 离线验证辅助脚本与样例代码放在一起(`check_*.py`、`verify_langfuse.py`)——它们是 + 一次性调试工具,不属于样例主流程。 diff --git a/a2a-t-sample/observability-sample/README.md b/a2a-t-sample/observability-sample/README.md index 826e83b2..daa515d5 100644 --- a/a2a-t-sample/observability-sample/README.md +++ b/a2a-t-sample/observability-sample/README.md @@ -1,98 +1,137 @@ -# observability-sample — A2A-T 可观测性演示(v3.0 装饰器架构) +# observability-sample — see what your A2A-T agents are actually doing -基于 `a2a-t-sdk[observability]` v3.0 的端到端可观测性样例:一行装饰器接线、零手动 OTel 代码。 +A runnable sample that shows the **full lifecycle of an A2A-T interaction** — a client +asks, a server answers, artifacts stream back — with every step automatically traced, +measured and logged by the `a2a-t-sdk` observability layer. -## 接线(v3.0) +There is **no manual OpenTelemetry code** in the sample: the server adds one wrapper, +the client adds one registration, and everything else is emitted by the SDK. -- Server:`A2ATRequestHandlerDecorator(LegacyRequestHandler(...))` —— 自动创建 SERVER 入口 Span - (从 `ServerCallContext.state["headers"]` 提取 traceparent 作为父上下文,无需 ASGI 中间件)、 - 包装 executor 产生 per-event Span、包装 push sender 产生 push Span -- Client:`register_client_factory(factory)` —— 之后 `factory.create(card)` 产出的 client 自动携带 - 观测 transport(CLIENT Span + traceparent 注入,经 `ClientCallContext.service_parameters` 落到 HTTP 头) -- OTel 自动配置:SDK 在首次使用时自动装配 Tracer/Meter/LoggerProvider(未配置 - `OTEL_EXPORTER_OTLP_ENDPOINT` 时回退 Console 导出),样例不含任何手动 setup 代码 +--- -## a2a-sdk 1.1.x handler caveat(必须 LegacyRequestHandler) +## What you can try -完整 per-event Span 富化需要 `LegacyRequestHandler`。a2a-sdk 1.1.2 默认的 `DefaultRequestHandler` -(V2)在构造期即捕获裸的 `agent_executor`/`_push_sender` 引用(ActiveTaskRegistry),装饰器构造期的 -executor/push 包装会被绕过。本样例固定使用 `LegacyRequestHandler`(服务端行为与 V2 兼容)。 +| Scenario (`--scenario=`) | Story | +|--------------------------|-------| +| `native` | A task flows end to end: the client submits, the server works and streams artifacts back. Watch the request light up on both sides. | +| `a2at` | Prompt generation with an LLM (a mock LLM by default — no API key needed) and a manual "L4" span around the SDK call. | +| `negotiation` | The agent **bargains before it commits**: two offer rounds happen over plain messages (no task yet), and only after both sides agree does the real task start and stream its result. | -## 运行(离线,默认 Console 输出) +## Quick start -```powershell -cd a2a-t-sample -$env:PYTHONPATH = "$pwd\observability-sample\src" -$env:A2AT_OBSERVABILITY_ENABLED = "true" # 观测总开关(默认即 true;置 false 时装饰器完全直通) +Prerequisites: you are in the repository root and [uv](https://docs.astral.sh/uv/) is installed. -# 终端 1:server(端口 8100) +```bash +# from the repo root — terminal 1: start the server (port 8100) +cd a2a-t-sample +export PYTHONPATH="$PWD/observability-sample/src" +export A2AT_OBSERVABILITY_ENABLED=true uv run python -m obs_sample.server_main -# 终端 2:client(原生 a2a-python 流量) +# terminal 2: run a client scenario +cd a2a-t-sample +export PYTHONPATH="$PWD/observability-sample/src" uv run python -m obs_sample.client_main --scenario=native - -# 或 client(A2ATClient 模板生成 + trace_facade L4 Span,需仓库 a2a-t-sample/.env: -# 不存在时从 env.example 复制并将 A2AT_LLM_API_KEY 置空(A2AT_LLM_API_KEY=),置空后自动走 mock LLM) -uv run python -m obs_sample.client_main --scenario=a2at - -# 协商场景:server 需以 negotiation 模式启动(返回 Negotiation-T 终态 Message) -uv run python -m obs_sample.server_main --scenario=negotiation -# 然后另一个终端: -uv run python -m obs_sample.client_main --scenario=negotiation ``` -`--scenario=a2at` 分支会调用一次幂等的 `setup()`(SDK 公共自动配置入口):L4 门面 Span -产生在首个 A2A 请求之前,需先完成 OTel 自动装配才不会丢失;native 场景无需任何调用, -首个请求即触发自动配置。 +Windows PowerShell: use `$env:PYTHONPATH = "$PWD\observability-sample\src"` instead of `export`. -端口可用 `A2AT_OBS_SAMPLE_PORT` 覆盖(默认 8100)。样例 client 会请求 push notification -(回环到 server 的 `/push-sink`),用于演示 `SendMessage-pushNotification` Span。 +You will see log lines on both terminals (`[server] ...` / `[client] ...`), and — with the +default configuration — every span printed as JSON on the server console. -## 预期输出(v3.0 Span 命名,a2a-java 对齐) +## Scenario walkthroughs -Client 终端(CLIENT Span): +### native — one task, both sides -- `SendStreamingMessage`(入口 Span,含 `extension.name`、`gen_ai.operation.name`、 - `gen_ai.client.operation.duration` 指标;traceparent 注入请求头) -- `SendStreamingMessage-event`(per-event,LINK 到入口 Span) -- `--scenario=a2at` 另有 `a2at.sdk.client.generate_task_prompt`(L4) -- `--scenario=negotiation` 另有 `SendStreamingMessage-negotiation`(PARENT 到入口 Span, - 含 `negotiation.id/round/max_rounds/performative` 属性;服务端返回 Negotiation-T 终态 Message 时自动识别) +1. Terminal 1 starts the server. Terminal 2 runs the client with `--scenario=native`. +2. The client submits a task; the server moves it through *submitted → working* and + streams an artifact back. +3. Each step appears as a span on **both** sides, joined into one trace + (the client passes a W3C `traceparent` to the server automatically). -Server 终端(SERVER/INTERNAL Span,与 Client 同 trace_id —— W3C traceparent 端到端贯通): +### a2at — generate a prompt with an LLM -- `SendStreamingMessage`(SERVER 入口 Span,父 = Client 入口 Span) -- `SendStreamingMessage-event`(per-event INTERNAL Span,父 = SERVER 入口 Span) -- `SendMessage-pushNotification`(CLIENT kind Span,LINK 到 SERVER 入口 Span) -- `--scenario=negotiation` 时 server 返回终态协商 Message(不产生 per-event Span 流) +```bash +uv run python -m obs_sample.client_main --scenario=a2at +``` -日志与指标: +The client generates a task prompt through the SDK's LLM facade. With an empty +`A2AT_LLM_API_KEY` in `a2a-t-sample/.env` a canned **mock LLM** answers, so this runs +fully offline. The LLM's token usage is measured automatically and attached to the trace. -- logger `a2at.observability` 自动经 OTLP/Console 导出(`task.status_changed` / `task.artifact` 等) -- 指标:`gen_ai.client.operation.duration`(client)、`a2at.task.request.duration`(side=client/server) +### negotiation — bargain first, execute later + +```bash +# terminal 1: restart the server in negotiation mode +uv run python -m obs_sample.server_main --scenario=negotiation +# terminal 2: +uv run python -m obs_sample.client_main --scenario=negotiation +``` -## 配置开关 +This is the real-world timing of a Negotiation-T deal: -| 环境变量 | 默认 | 说明 | -| --- | --- | --- | -| `A2AT_OBSERVABILITY_ENABLED` | `true` | 观测总开关;`false` → 装饰器完全直通(无任何观测) | -| `A2AT_TRACE_ENABLED` | `true` | Trace 信号开关;`false` → 不产生 Span,指标/日志仍工作 | -| `A2AT_METRIC_ENABLED` | `true` | Metric 信号开关 | -| `A2AT_LOG_ENABLED` | `true` | Log 信号开关 | -| `OTEL_EXPORTER_OTLP_ENDPOINT` | 未设置 | 未设置 → Console 导出(dev 模式) | -| `A2AT_EXPORTER_PROTOCOL` | `grpc` | OTLP 协议(`grpc` / `http/protobuf`) | -| `OTEL_SERVICE_NAME`(或 `A2AT_SERVICE_NAME`) | `a2a-t-agent` | Resource `service.name` | -| `OTEL_INSTRUMENTATION_A2AT_SDK_ENABLED` | `true` | OTel 主开关;`false` → 全部 NoOp 直通 | +1. **Round 1** — the client proposes; the server counters with a different offer and + disconnects. *No task exists yet — bargaining happens over plain messages.* +2. **Round 2** — the client reconnects, accepts the counter-offer; the server confirms + the agreement and disconnects again. +3. **The task starts only after agreement** — a separate request now runs the full + task lifecycle and streams the result. -## 切换 OTLP 后端 +Each round is an independent exchange; the sample ties them together so you can follow +the whole deal in one place (see Langfuse below). -```powershell -$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://collector:4317" -``` +## Send everything to Langfuse (optional) -(需另装 `opentelemetry-exporter-otlp-proto-grpc`;默认 Console 输出无需任何额外依赖。) +Set three variables before starting **both** processes and every span lands in your +Langfuse project as one browsable trace per interaction: -## 说明 +```bash +export LANGFUSE_PUBLIC_KEY="pk-lf-..." +export LANGFUSE_SECRET_KEY="sk-lf-..." +export LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" +``` -- 断言式端到端验证见 `tests/integration/test_e2e_observability.py`(`A2AT_TEST_A2A=1`)。 -- a2at 场景的 mock LLM 资源复用 `../subscribe-incident/resources/mock_responses`。 +In the Langfuse UI you will see the client and server spans of every round and the final +task on a single timeline — including who proposed what, which round was accepted, and +how long each step took. + +## What gets recorded (cheat sheet) + +| You do | The SDK records | +|--------|-----------------| +| Client sends a request | `SendStreamingMessage` span (CLIENT) + duration metric | +| Server receives it | `SendStreamingMessage` span (SERVER), child of the client's | +| Server streams an event | `SendStreamingMessage-event` span, linked to the entry | +| A negotiation message is sent / received | `SendStreamingMessage-negotiation` span with round & performative | +| Server pushes a notification | `SendMessage-pushNotification` span, linked to the server entry | +| The LLM generates tokens | token-usage metric + `gen_ai.usage.*` on the next trace | +| Anything noteworthy | a structured log line on logger `a2at.observability` | + +Signals can be toggled independently — see the environment variables below. + +## Environment variables + +Commonly used: + +| Variable | Default | Meaning | +|----------|---------|---------| +| `A2AT_OBSERVABILITY_ENABLED` | `true` | Master switch — `false` makes the SDK fully transparent | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | *(unset)* | Send spans/metrics/logs to an OTLP collector instead of the console | +| `A2AT_CONSOLE_EXPORTER` | `true` | `false` disables the dev-mode console fallback (no stdout JSON) | +| `OTEL_SERVICE_NAME` | `a2a-t-agent` | How this process is named in the backend | +| `A2AT_OBS_SAMPLE_PORT` | `8100` | Sample server port | +| `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` | *(unset)* | Enable the Langfuse backend | + +Advanced (rarely needed): `A2AT_TRACE_ENABLED`, `A2AT_METRIC_ENABLED`, `A2AT_LOG_ENABLED` +(per-signal switches — e.g. trace off keeps metrics and logs alive), `A2AT_EXPORTER_PROTOCOL` +(`grpc` / `http/protobuf`), `OTEL_INSTRUMENTATION_A2AT_SDK_ENABLED=false` (hard disable). + +## Notes + +- The server uses a2a-sdk's `LegacyRequestHandler` on purpose: the newer + `DefaultRequestHandler` (a2a-sdk 1.1.x V2) captures raw executor/push-sender + references at construction time, which would bypass the SDK's wrappers. +- Push notifications are echoed back to the server's `/push-sink` endpoint, which is + why the sample can show the `SendMessage-pushNotification` span end to end. +- Offline verification helpers live next to the sample code (`check_*.py`, + `verify_langfuse.py`) — they are one-off debugging tools, not part of the sample flow. From db37a7c5c43677da69a00fad9c13d3900866770e Mon Sep 17 00:00:00 2001 From: z00930736 Date: Thu, 15 Oct 2026 11:56:56 +0800 Subject: [PATCH 2/7] feat(observability): native support for DefaultRequestHandlerV2 + sample doc updates - decorator detects the V2 shape (_active_task_registry) and rebuilds the ActiveTaskRegistry with the decorated executor/push sender (D9): per-event and push spans now work on the OFFICIAL default handler - push sender wrap: install the _dispatch_notification wrapper in the V2 branch too (V2 pushes from the event pump via send_notification -> dispatch) - send_notification forwards without a second span when the dispatch wrapper handles fanout (dedup); _warn_if_v2_handler replaced by the adaptive INFO - sample server + E2E migrate to the official DefaultRequestHandler (V2 protocol: executors enqueue the initial Task snapshot first); E2E updated for the +initial-task-snapshot event and task-kind event spans - includes the continued review work: payload redaction order, config gates, log/trace plumbing adjustments and their tests Signed-off-by: z00930736 --- README.md | 157 +++++++++++++++++- .../src/obs_sample/server_main.py | 27 ++- .../observability/server/handler_decorator.py | 68 +++++--- tests/integration/test_e2e_observability.py | 57 ++++--- tests/observability/test_handler_decorator.py | 57 +++++-- 5 files changed, 284 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 18038357..e954ba86 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,8 @@ -