Documentation
¶
Overview ¶
otelmap.go —— 轨迹行 → OTel 属性的**契约表**(属性名 / 来源字段 / 取值域 / 基数上界)。
为什么单独一张表 ¶
otel.go 的映射函数回答「这一行导出去长什么样」,这张表回答「**宿主可以依赖什么**」。 分开是因为失效形态不同:映射写错会出可见的错值;契约缺失**不会报错**——宿主建看板时 只能自己猜字段名,于是同一个语义在 A 宿主叫 @turn_id@、在 B 宿主叫 @turn@,跨宿主的 看板与告警全部落空,而遥测数据照常上行。
判据:每一项都必须「有主」 ¶
判据在 @otelmap_test.go@,且是**双向**的:映射函数写出了表里没有的属性 ⇒ 红;表里列了 映射函数产不出的属性 ⇒ 也红(后者防的是「表变成了许愿池」)。另逐项判四件事:来源字段 非空、取值域写出、基数上界写出依据、属性名唯一。缺一即红。
Go 类型为什么进契约 ¶
取值域的形态写的是 **Go 类型名**(string / int64 / float64 / bool):宿主导出时按类型 选 OTel 属性类型,类型不符的属性在多数后端里**静默丢弃**(不报错、不告警),表现为 「数据看起来上行成功、看板上就是没有」。写成 Go 类型名让用例拿反射直接比对映射函数的 真实产物——这是「表」与「代码」之间唯一不会漂的对拍方式。
哪些字段不许进 metric label ¶
只有**本仓枚举得出、也给得出上界**的属性才允许作 metric label(@LabelSafe@ 蕴含 取值域是枚举、且 @Card.Max@ 不小于取值数)。理由不是洁癖:label 的代价是时序数按取值数 **乘**上去,一个无界 label 就能让指标后端按会话数膨胀。@session_id@ / @agent_slug@ 是 典型,列在 @ForbiddenLabels@ 里——而它们**根本不在 Row 上**:结构上装不进去,不靠纪律拦。
Package trajectory 事件流 → 轨迹行的投影器(机制面)。
「这一轮到底发生了什么、每段花了多久」这件事,宿主此前只能自己从事件流里拼: 起止时刻靠猜,工具调用与结果按 call_id 手工配对,配对缺了没人知道。各宿主 各拼一套,于是同一个问题的答案会漂——尤其是**时长这一个数**:有人拿载荷 体积估、有人拿 token 数除、有人按事件时刻相减,三种口径混进同一张表, 谁也说不清哪一列是实测、哪一列是猜的。
本包把这件事收成一条机制面:**吃本仓 eventbus 的事件,产出轨迹行**。
三条纪律:
**实测与估算不得混同**(本包存在的核心理由)。行的时长**只有一个源**: 起止时刻相减。Row 里**不存**时长字段——时长是 Duration() 现算的。 于是「按载荷体积估一个时长」这件事**无处安放**:不是靠纪律约束,是 结构上装不进去。没有实测值就是没有实测值(Duration 返回 ok=false), 不拿 0 冒充「瞬时完成」,也不拿估算值填进实测字段。
**配对不完整要显形**。工具调用与结果按 call_id 配对;只有调用没有结果、 结果重复、结果没有对应调用、同号重复声明——四种缺口都进 Issues(), 不静默丢弃。
**未完成不是完成**。只有开始没有结束的行,状态是 open(未收口), 不是「时长 0 的已完成行」。Finalize() 把「不会再有新事件」落成显式事实, 未收口的行原样报出。
事件流是追加式的,故投影是**增量**的:先喂一半再喂后半,既有行被就地更新 (补上结束时刻、时长随之变成实测),而不是追加出第二条行。
边界:本包是机制面,不含任何宿主领域知识;**不做前端渲染**——各宿主技术栈 不同,轨迹行是数据不是视图(要文案就用 Options.Summarize 注入)。
Index ¶
Constants ¶
const ( // AttrRowID 行标识(与 Row.ID 同值)。 AttrRowID = "harness.trajectory.row_id" // AttrKind 行类目(turn / step / tool_call / tool_result)。 AttrKind = "harness.trajectory.kind" // AttrRowStatus 行的收口状态(open / closed / orphan)。 AttrRowStatus = "harness.trajectory.status" // AttrTurnSeq 所属轮次序号(1 起;轮次外的事件不写此项)。 AttrTurnSeq = "harness.turn.seq" // AttrStepID 所属环节号(仅带环节号的行)。 AttrStepID = "harness.step.id" // AttrToolName 工具名(仅工具行;**属性**而不是 span 名,见头注纪律 1)。 AttrToolName = "harness.tool.name" // AttrToolCallID 工具调用标识(配对键,宿主跨 span join 用)。 AttrToolCallID = "harness.tool.call_id" // AttrToolPairing 配对状态(paired / missing_result / duplicate_result / …)。 AttrToolPairing = "harness.tool.pairing" // AttrToolDupResults 重复结果计数(>0 才写)。 AttrToolDupResults = "harness.tool.duplicate_results" // AttrDurationMs 实测时长(毫秒,float64)。**仅实测时出现**(见头注纪律 2)。 AttrDurationMs = "harness.trajectory.duration_ms" // AttrSummary 行摘要(宿主可用 Options.Summarize 覆盖;空则不写)。 AttrSummary = "harness.trajectory.summary" )
OTel 属性名(本仓命名空间)。宿主据此建 dashboard 与告警;改名即破坏兼容。
const TraceBoundary = TracePerTurn
TraceBoundary 本仓选定的 trace 切分单位。
依据见 otel.go 头注「trace 边界」:选「轮」不是习惯,是因为 Row.ID 的唯一性 范围恰好是轮内——把边界挪到环节,标识唯一性与 trace 内唯一性就不再是同一个范围。
Variables ¶
var AttrSpecs = []AttrSpec{ { Name: AttrRowID, Source: "Row.ID", Domain: Domain{Kind: "string", Note: "行标识;形态由投影器决定(turn#N / step#N / tool#<call_id> / <kind>#-<seq>),唯一范围是轮内"}, Card: Cardinality{Why: "取值随行数增长(行数由事件流长度决定),本仓定不出上界"}, }, { Name: AttrKind, Source: "Row.Kind", Domain: Domain{Kind: "string", Values: []string{ string(KindTurn), string(KindStep), string(KindToolCall), string(KindToolResult), }}, Card: Cardinality{Max: 4, Why: "Kind 词表固定四值(与 Span.Name 同一词表:新增类目必须同步这里与用例)"}, LabelSafe: true, }, { Name: AttrRowStatus, Source: "Row.Status", Domain: Domain{Kind: "string", Values: []string{ string(StatusOpen), string(StatusClosed), string(StatusOrphan), }}, Card: Cardinality{Max: 3, Why: "Status 词表固定三值"}, LabelSafe: true, }, { Name: AttrTurnSeq, Source: "Row.Turn", Domain: Domain{Kind: "int64", Note: "1 起的轮次序号;轮次外的事件(Turn 为 0)不写此项"}, Card: Cardinality{Why: "随会话轮数增长,本仓定不出上界"}, }, { Name: AttrStepID, Source: "Row.StepID(Row.HasStep 为真时才写:环节号 0 是合法值,不能靠零值判有无)", Domain: Domain{Kind: "int64", Note: "环节号,由调用方给定(uint64 全域)"}, Card: Cardinality{Why: "取值由调用方决定,本仓定不出上界"}, }, { Name: AttrToolName, Source: "Row.Tool(仅工具行)", Domain: Domain{Kind: "string", Note: "工具名,来自工具调用事件载荷"}, Card: Cardinality{Why: "取值数由宿主的工具目录规模决定,本仓看不到那个目录;宿主若要按工具聚合,应在 label 侧用自己的白名单收敛"}, }, { Name: AttrToolCallID, Source: "Row.CallID(仅工具行)", Domain: Domain{Kind: "string", Note: "配对键:一次工具调用一个值,宿主跨 span join 用"}, Card: Cardinality{Why: "调用次数无上界"}, }, { Name: AttrToolPairing, Source: "Row.Pairing(仅工具行,且不为 PairingNone)", Domain: Domain{Kind: "string", Values: []string{ string(PairingPaired), string(PairingMissingResult), string(PairingDuplicateResult), string(PairingUnpairedResult), string(PairingDuplicateCall), }}, Card: Cardinality{Max: 5, Why: "Pairing 词表五值(PairingNone 是「不适用」,不写进属性)"}, LabelSafe: true, }, { Name: AttrToolDupResults, Source: "Row.DupResults(仅工具行,且 >0)", Domain: Domain{Kind: "int64", Note: "重复结果计数,1 起;没有重复就没有这个属性"}, Card: Cardinality{Why: "重复次数无上界(正常情况下恒为 1)"}, }, { Name: AttrDurationMs, Source: "Row.Duration()(StartAt 与 EndAt 相减,不是单独存的字段)", Domain: Domain{Kind: "float64", Note: "实测毫秒;**仅实测时出现**——没有实测就没有这个属性,不写 0、不估一个"}, Card: Cardinality{Why: "连续值,本仓定不出上界"}, }, { Name: AttrSummary, Source: "Row.Summary(可由 Options.Summarize 覆盖)", Domain: Domain{Kind: "string", Note: "宿主注入的展示文案;内核默认标签不含任何领域词"}, Card: Cardinality{Why: "自由文本,本仓定不出上界"}, }, }
AttrSpecs 映射契约表(顺序 = otel.go 的常量声明顺序,便于逐行对读)。
var ForbiddenLabels = []ForbiddenLabel{
{
Name: "session_id",
Why: "一次会话一个取值:进 label 会让指标时序数按会话数膨胀,且会话标识是可关联标识——进了指标后端就等于把它变成长期可查询的面",
},
{
Name: "agent_slug",
Why: "取值数随部署的 agent 目录增长,本仓定不出上界;同一部署内它也更像维度而不是枚举",
},
{
Name: "payload",
Why: "事件载荷:自由文本、基数无界,且可能含未脱敏内容——映射面刻意不读它(见 otel.go 头注「映射面不含 payload」)",
},
}
ForbiddenLabels 禁区清单。
这三项都不在 Row 上——写在这里是给宿主的:它们看起来「很像维度」(也确实是有用的 关联键),但落进 metric label 就变成无界基数。要关联会话,用 trace_id(真相源在 @oekit/tracing@),那是**标识**不是维度。
Functions ¶
This section is empty.
Types ¶
type AttrSpec ¶
type AttrSpec struct {
// Name 属性名(必是 otel.go 里的某个 Attr* 常量,且全表不重复)。
Name string
// Source 来源字段:Row 上的哪一个字段(派生值写清怎么算出来的)。
Source string
// Domain 取值域。
Domain Domain
// Card 基数上界。
Card Cardinality
// LabelSafe 能否作为 metric label;为真必须「枚举取值 + 本仓给得出上界」。
LabelSafe bool
}
AttrSpec 一个属性的映射契约。
type Cardinality ¶
type Cardinality struct {
// Max 本仓能保证的取值上界;0 = 本仓定不出上界。
Max int
// Why 上界依据(Max 为 0 时写清为什么定不出)。
Why string
}
Cardinality 属性基数上界。
type Domain ¶
type Domain struct {
// Kind 取值形态,取 **Go 类型名**(string / int64 / float64 / bool)。
// 见头注「Go 类型为什么进契约」。
Kind string
// Values 枚举取值(按本仓词表顺序);空 = 非枚举。
Values []string
// Note 取值域说明。非枚举必写(枚举的取值域已由 Values 表达完整)。
Note string
}
Domain 属性取值域。
type ForbiddenLabel ¶
ForbiddenLabel 明确禁止作为 metric label 的语义(宿主不得自行补进 label)。
type Issue ¶
type Issue struct {
// Pairing 缺口类目(missing_result / duplicate_result / unpaired_result /
// duplicate_call)。
Pairing Pairing
// CallID 相关工具调用标识(无关联调用时为空)。
CallID string
// Seq 相关行的序号。
Seq int
}
Issue 配对缺口(可观测面):配对不完整时显式报出,不静默丢弃。
type Kind ¶
type Kind string
Kind 轨迹行的类目。
const ( // KindTurn 轮次行:一轮 turn 的起止(turn_started / turn_ended)。 KindTurn Kind = "turn" // KindStep 步骤行:一个环节的起止(step_started / step_finished)。 KindStep Kind = "step" // KindToolCall 工具调用行:调用时刻为起,配对到的结果时刻为止。 KindToolCall Kind = "tool_call" // KindToolResult 工具结果行:**只在结果没有对应调用时**出现—— // 配对缺口要显形,不能因为「没有调用行可挂」就把结果丢掉。 KindToolResult Kind = "tool_result" )
type Options ¶
type Options struct {
// Summarize 摘要注入点:宿主自定义行标签(展示文案归宿主,内核不认识
// 任何领域词)。返回空串时沿用本包的通用标签;nil 时全程用通用标签。
//
// 只在行创建时调用一次——行的可读字段(类目、标识、工具名、时刻)在创建
// 后不再变化,故摘要不需要跟着配对状态刷新。
Summarize func(Row) string
}
Options 投影选项。
type Pairing ¶
type Pairing string
Pairing 工具调用与结果的配对状态(非工具行恒为 PairingNone)。
const ( // PairingNone 不适用(非工具行)。 PairingNone Pairing = "" // PairingPaired 调用与结果已配对:起止齐备,时长为实测。 PairingPaired Pairing = "paired" // PairingMissingResult 尚未观察到结果。可能仍在途;投影收口后仍如此, // 即确认为「等了也没等到」。 PairingMissingResult Pairing = "missing_result" // PairingDuplicateResult 同一 call_id 收到第二个及以后的结果。 PairingDuplicateResult Pairing = "duplicate_result" // PairingUnpairedResult 结果没有对应的调用。 PairingUnpairedResult Pairing = "unpaired_result" // PairingDuplicateCall 同一 call_id 收到第二个调用声明。 PairingDuplicateCall Pairing = "duplicate_call" )
type Projector ¶
type Projector struct {
// contains filtered or unexported fields
}
Projector 增量投影器:事件流 → 轨迹行。
并发安全:事件流可能由多个生产者投递,故 Apply 内部加锁。
func (*Projector) Apply ¶
Apply 投一条事件(增量投影)。
已经被投过的事件不会被重复计入:后续事件只会**更新**既有行(补结束时刻、 把配对状态从「未配对」改成「已配对」),不会追加出第二条行。
func (*Projector) Finalize ¶
Finalize 收口投影:把「不会再有新事件」落成显式事实。
返回仍未收口的行(未完成行)。这些行是 open 态:它们有起点、缺终点, 不是「时长 0 的已完成行」。工具调用行若此时仍无结果,会被记成 missing_result 缺口——到这一步才知道「等不到」是真的等不到。
不锁死投影器:宿主若继续投事件,投影照常更新,相应缺口也会被清掉。
type Row ¶
type Row struct {
// Seq 行序号(1 起,按创建顺序):宿主据此排序,投影内稳定。
Seq int
// Kind 行类目。
Kind Kind
// ID 行标识:轮次行 "turn#1";带环节号的步骤行 "step#7";
// 工具行 "tool#<call_id>";无自然键的行用 "step#-<seq>" 形式兜底
// ("-" 前缀表示「这个标识是投影生成的,不是业务自然键」)。
// 格式的唯一生成点是 turnRowID / stepRowID / toolRowID / seqID——otel 的
// 父子关系复用同一组函数,另写一份格式父子就会连不上。
ID string
// Turn 所属轮次序号(1 起);轮次之外的事件为 0。
Turn int
// HasStep 是否带环节号(环节号 0 是合法值,故用独立标志位表达「有没有」)。
HasStep bool
// StepID 所属环节号(HasStep 为真时有效)。
StepID uint64
// CallID 工具调用标识(仅工具行;配对键)。
CallID string
// Tool 工具名(仅工具行)。
Tool string
// StartAt 起始时刻(取自事件时刻;缺失时零值)。
StartAt time.Time
// EndAt 结束时刻(取自事件时刻;未收口时零值)。
EndAt time.Time
// Status 收口状态。
Status Status
// Pairing 配对状态(仅工具行)。
Pairing Pairing
// DupResults 重复结果计数(同一 call_id 收到第二个及以后的结果时累加)。
DupResults int
// Summary 行摘要(默认通用标签;宿主可用 Options.Summarize 覆盖)。
Summary string
}
Row 轨迹行:一轮 turn 里的一段有起止的过程。
行里**没有**时长字段——时长由 Duration() 从起止时刻现算(见包头纪律 1)。 存一个时长字段就会多出第二个真相:事件时刻与存下来的时长各说各话, 而估算值也就能混进同一个字段冒充实测。
type Span ¶
type Span struct {
// Name span 名(低基数,取值域见 Kind)。
Name string
// ParentID 父 span 的行标识(与 Row.ID 同源,见头注「trace 边界」);
// 空 = 根,宿主应为它开一个新 trace。
ParentID string
// Status span 状态。
Status SpanStatus
// StartAt 起点(未收口时为零值,宿主导出时可自行取当前时刻)。
StartAt time.Time
// EndAt 终点(未收口时为零值)。
EndAt time.Time
// Attributes 属性表(键取本文件的 Attr* 常量,值是 string/int64/float64)。
Attributes map[string]any
}
Span 一行轨迹的 span 表达(不含 exporter 相关字段:那是投递侧的事)。
type SpanStatus ¶
type SpanStatus string
SpanStatus span 状态(OTel 的三值词表)。
const ( // SpanStatusUnset 未定:这一行还没收口,或「仍在等结果」。 // // 缺省就是这个——把未定硬记成 error 会让真正的事故淹在噪声里。 SpanStatusUnset SpanStatus = "unset" // SpanStatusOK 已完成且没有观察到问题。 SpanStatusOK SpanStatus = "ok" // SpanStatusError 观察到**发生过**的问题(不是「还没等到」)。 SpanStatusError SpanStatus = "error" )
type TraceGranularity ¶
type TraceGranularity string
TraceGranularity trace 的切分单位。
const ( // TracePerTurn 一轮 turn 一个 trace(**本仓选它**)。 TracePerTurn TraceGranularity = "turn" // TracePerStep 一个环节一个 trace(本仓不选:行标识只在轮内唯一, // 同号环节会在两个 trace 里撞同一个标识)。 TracePerStep TraceGranularity = "step" )