Show the trace loop from an error code to a log entry and HTTP envelope.
The example keeps the two modules decoupled and checks that one trace id appears on both sides. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -81,3 +81,5 @@ replace logging-system => ../Logging-System
|
|||||||
```
|
```
|
||||||
|
|
||||||
远程:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Logging-System.git`
|
远程:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Logging-System.git`
|
||||||
|
|
||||||
|
与错误码系统的追溯闭环见 [docs/integration.md](docs/integration.md)。可运行示例:`go test ./examples/trace`(两个仓库需为同级目录)。
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 错误码、日志与 trace_id
|
||||||
|
|
||||||
|
两个库互不导入。错误码库负责稳定的码和 HTTP 信封,日志库负责把同一次调用的运行记录留下来。对齐点只有两个:
|
||||||
|
|
||||||
|
- 追踪头都是 `X-Trace-Id`
|
||||||
|
- 日志通过 `ErrorCode()` / `ErrorFields()` 识别错误,不需要 `import error-code-system`
|
||||||
|
|
||||||
|
## 一次失败怎么串起来
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP 中间件生成或沿用 X-Trace-Id
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
errcode.Wrap(...) 得到规范码
|
||||||
|
│
|
||||||
|
├─ httpx.WriteError → {"error":{"code","message","reason","trace_id"}}
|
||||||
|
│
|
||||||
|
└─ log.WithContext(ctx).Error(msg, err)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
日志条目含 trace_id + code + 字段
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
HTTP sink 批量 POST 到 :8092
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
GET /v1/logs?trace_id=... 或 ?q=agent.provider_unavailable
|
||||||
|
```
|
||||||
|
|
||||||
|
查问题先拿响应里的 `trace_id` 或 `code`。`trace_id` 能拉出这一次调用的全部日志;`code` 能跨请求看同一类失败。副作用是 `possible` 或 `unknown` 的码不要无条件重放,先看日志里这次执行写过什么。
|
||||||
|
|
||||||
|
## 最小示例
|
||||||
|
|
||||||
|
可运行副本:`Logging-System/examples/trace`(需要两个仓库是同级目录)。
|
||||||
|
|
||||||
|
```go
|
||||||
|
reg := errcode.NewRegistry()
|
||||||
|
domains.RegisterAll(reg)
|
||||||
|
|
||||||
|
cfg, _ := config.Load("log.json") // 见 Logging-System/docs/example.json
|
||||||
|
log, _ := config.Build(cfg)
|
||||||
|
defer log.Close()
|
||||||
|
|
||||||
|
ctx := logsys.WithTrace(r.Context(), httpx.TraceID(r))
|
||||||
|
err := errcode.Wrap(cause, errcode.AgentProviderUnavailable, "模型不可达", "model", "glm-4")
|
||||||
|
log.WithContext(ctx).Error("provider down", err)
|
||||||
|
|
||||||
|
httpx.WriteError(reg, w, r, err)
|
||||||
|
```
|
||||||
|
|
||||||
|
日志条目里会有 `trace_id=...` 和 `code=agent.provider_unavailable`。采集器侧:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
logctl query --addr http://127.0.0.1:8092 --trace <响应里的 trace_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入现有仓库时
|
||||||
|
|
||||||
|
本轮不改 Agent-Native / Agent-Squad。对应关系已经写在错误码仓库:
|
||||||
|
|
||||||
|
- `docs/agent-native-mapping.md`
|
||||||
|
- `docs/agent-squad-mapping.md`
|
||||||
|
|
||||||
|
接入时保持现有事件名和 `AssessStepFailure` 分类,只把对外的错误字符串换成规范码,并把 `run_id` 写入日志字段。`run_id` 回答「哪一次编排」,`trace_id` 回答「哪一次请求」,两者都保留。
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module logging-system/examples/trace
|
||||||
|
|
||||||
|
go 1.27
|
||||||
|
|
||||||
|
require (
|
||||||
|
error-code-system v0.0.0
|
||||||
|
logging-system v0.0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace error-code-system => ../../../Error-Code-System
|
||||||
|
|
||||||
|
replace logging-system => ../..
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"error-code-system/domains"
|
||||||
|
"error-code-system/errcode"
|
||||||
|
"error-code-system/httpx"
|
||||||
|
"logging-system/logsys"
|
||||||
|
"logging-system/sink"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrorCodeReachesLogAndHTTP(t *testing.T) {
|
||||||
|
reg := errcode.NewRegistry()
|
||||||
|
domains.RegisterAll(reg)
|
||||||
|
mem := &sink.Memory{}
|
||||||
|
log := logsys.New(logsys.Options{Service: "agent-squad", Level: logsys.LevelInfo, Sinks: []logsys.Sink{mem}})
|
||||||
|
|
||||||
|
cause := errors.New("dial tcp: connection refused")
|
||||||
|
err := errcode.Wrap(cause, errcode.AgentProviderUnavailable, "模型不可达", "model", "glm-4")
|
||||||
|
const traceID = "trace-loop-0001"
|
||||||
|
log.WithTraceID(traceID).Error("provider down", err)
|
||||||
|
|
||||||
|
got := mem.Snapshot()
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("logs %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0].TraceID != traceID || got[0].Code != string(errcode.AgentProviderUnavailable) {
|
||||||
|
t.Fatalf("entry %+v", got[0])
|
||||||
|
}
|
||||||
|
if got[0].Fields["model"] != "glm-4" {
|
||||||
|
t.Fatalf("fields %+v", got[0].Fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v1/runs", nil)
|
||||||
|
req.Header.Set(httpx.TraceHeader, traceID)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
httpx.WriteError(reg, rec, req, err)
|
||||||
|
if rec.Code != http.StatusServiceUnavailable {
|
||||||
|
t.Fatalf("status %d", rec.Code)
|
||||||
|
}
|
||||||
|
var env httpx.Envelope
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if env.Error.Code != got[0].Code || env.Error.TraceID != got[0].TraceID {
|
||||||
|
t.Fatalf("envelope %+v log code %s trace %s", env.Error, got[0].Code, got[0].TraceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user