| 项 | 值 |
|---|---|
| 项目 | dzs/dzs-price-calculator |
| 分支 | v1.0.25 |
| 基准 commit(base) | c423ea8ec772b9021d2f962310b52656fed15b84 |
| 目标 commit(head) | 4db690f252460d1ff83803aab90835fe8717f619 |
| 变更文件 | 5 个(1 新增 / 4 修改) |
| 质检问题对比 | 修复 0 条 / 新增 3 条(工具权威数据,issues_diff) |
| 版本性质 | 纯新增「可观测性」能力(OpenTelemetry 链路 + 指标,OTLP/HTTP 推送 OpenObserve),无缺陷修复 |
| 文件 | 变更 | 说明 |
|---|---|---|
internal/obs/obs.go | 新增 +174 行 | 观测包:trace + metric 导出器初始化、指标定义、fail-open 门禁 |
cmd/calculator/main.go | +11 行 | 启动时 obs.Init,进程退出前带 5s 超时 flush/shutdown |
internal/scheduler/scheduler.go | +12 行 | 每轮计算包一个 calc.round span + 轮次耗时/结果指标 |
go.mod | +7 直接依赖 / +约 18 间接依赖 | 引入 go.opentelemetry.io/otel* v1.46.0(sdk / metric / trace / otlptracehttp / otlpmetrichttp) |
go.sum | 大量记账变更 | 新增 OTel 及 grpc / genproto / protobuf / x/net 等传递依赖条目 |
变更面小且集中:只有一个新包 + 两处接线点,无需全量分析,下文全部围绕新增代码展开。
internal/obs 包(obs.go):
Init(ctx, service):读 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_METRICS_ENDPOINT(缺失时从 traces 端点推导 metrics),构造 OTLP/HTTP 导出器,Authorization 头来自 DZS_OTEL_AUTH;返回 shutdown。calc_round_duration_ms、calc_rounds_total、calc_commodity_total、calc_outlier_removed_total。Tracer、Enabled、RecordRound、RecordOutlierRemoved。Warn 不返回错误;RecordRound/RecordOutlierRemoved 有 Enabled + nil 双重门禁,未接入时零副作用、不会 panic。main.go 在 config.LoadEnvFile()(.env 已注入进程环境)之后、config.Load()(DB 配置)之前调用 obs.Init,观测配置不依赖数据库,符合 spec 中「不得成为启动硬依赖」的要求;观测 env 放在 .env 中可被正确读取,此处无「先读 env 后加载 .env」的时序缺陷。0 条。
base..head 的问题集对比显示:head 中没有任何 base 已存在、head 已消失的问题。即本版本未修复任何历史缺陷,其价值全部来自新增观测能力。
| # | 严重度 | 类型 | 位置 | 描述 | 建议 |
|---|---|---|---|---|---|
| 1 | Medium | Dependency · 高耦合 | internal/obs/obs.go:48(Init) | Init 直接依赖 33 个函数,超过阈值 10 | 拆分函数或引入接口降低耦合 |
| 2 | Low | Dependency · 扇入扇出异常 | internal/obs/obs.go:48(Init) | Init fan-in 1 / fan-out 33(合计 34),超异常阈值 | 重构以平衡调用结构 |
| 3 | Low | Maintainability · 重复代码块 | internal/obs/obs.go:70 | 重复 4 行代码块(出现 2 次) | 抽取为共享函数 |
定性:3 条问题全部落在本版本新增的同一个文件 internal/obs/obs.go,无一涉及既有业务代码——属于「新增代码自带的结构性问题」,而非回归。
根因与整改方向(人工补充,指向具体改法):
Init 一个函数同时承担了 6 类职责:端点解析与归一化、鉴权头组装、resource 构造、trace provider 构造、metric provider 构造、shutdown 聚合。拆成 initTracing() / initMetrics() / newResource() 三个小函数即可同时消除「高耦合」与「扇出异常」两条。otlptracehttp.New(...) 与 otlpmetrichttp.New(...) 两段近乎同构的「导出器构造 + 失败 Warn + provider 构造 + 挂 shutdown」逻辑(obs.go 约 L68–L100),可抽为带泛型/回调的 buildExporter。说明:以下结论均给出代码证据;涉及行号处已通过行切片核对(±1 行)。
calc_commodity_total 指标语义与数据不符:把「品类×日期对数」当作「品类数」internal/scheduler/scheduler.go · runCalculationCycle(约 L280)calcCount = len(affected);而 affected := mergeAffected(changed, estimateAffected) 的元素类型是 repository.CommodityDate(品类 + 日期),下方 for _, ad := range recalculated { key := repository.CommodityDateKey{CommodityID: ad.CommodityID, PriceDate: ad.PriceDate} } 可确认其为二元组。internal/obs/obs.go L127/L136,calc_commodity_total 描述为「已计算品类数(按 result)」,参数名 commodities。calc_commodity_date_total(并在描述中写明是 品类×日期 对数),要么在 scheduler 侧传 len(groupAffectedByCommodity(affected)) 的真实品类数。roundResult 仍为 "ok"runCalculationCycle 在 L177 注册 defer obs.RecordRound(roundResult, ...),roundResult 初值 "ok";其后的两条错误早退分支未改该值:
GetWindowFingerprints 失败 → logrus...Error("查询计算窗口指纹失败,跳过本轮"); returncalcStateRepo.Load 失败 → logrus...Error("读取计算状态失败,跳过本轮"); returnresult=ok,监控看不见异常,与"引入观测以发现故障"的目标相悖。failCount > 0 && successCount == 0 才置 "error"(L280 附近),部分失败(如 9 成功 1 失败)同样记为 "ok";另外 if !c.running.TryLock() 的「上一轮未完成」分支(L160 附近)在 defer 注册之前 return,完全不上报,长期抢占时监控无感知。roundResult = "error"(或返回 error 由 defer 统一判定);为 result 增加 partial 或把 fail_count 作为独立属性/指标上报。RecordOutlierRemoved 定义后全仓无调用 → calc_outlier_removed_total 永不产出obs.RecordOutlierRemoved(obs.go L144–L150)与指标 calc_outlier_removed_total(L122–L123)仅在本包内出现;全仓 obs. 外部调用点仅 3 处(main.go 的 obs.Init、scheduler.go 的 obs.Tracer.Start / obs.RecordRound)。internal/calculator/outlier.go(DetectOutliers*)与 internal/calculator/pipeline.go(globalPreFilter、aggregateSourceGroup),而 internal/calculator 包并未 import obs,接线点缺失。pipeline.go 的剔除点回传剔除条数,由调用方(scheduler)转调 obs.RecordOutlierRemoved;若不打算本期实现,先删掉该函数与指标,避免误导。Init 中 if Tracer == nil 是死代码 + 未配置观测时完全静默obs.go L36 var Tracer trace.Tracer = otel.Tracer("noop") —— Tracer 在包初始化时已赋非 nil 值,L102 附近的 if Tracer == nil { Tracer = otel.Tracer(service) } 永不成立(补充:otel.Tracer 返回的是当时全局 provider 的 tracer,之后再 SetTracerProvider 不会让旧 tracer 生效,因此该兜底分支即使成立也无实际意义)。Init 直接 return noop(L50–L53),不打印任何日志。fail-open 的好处是"不阻断业务",代价是配置漏写/写错时现场无任何痕迹,只能靠"看板上没数据"反推。logrus.Debug/Info("观测未配置(OTLP 端点为空),已退化为 no-op")。resolveEndpoint(obs.go L164–L174)以 strings.HasSuffix(v, signalPath) 判定"是否已是完整信号路径",否则直接拼接:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://o2/api/default/v1/traces?org=x → 后缀判定失败 → 变成 .../v1/traces?org=x/v1/traces → 导出 404(与注释里"避免 404"的目的相反)。metricEndpoint()(L152–L162)用 strings.Replace 推导 /v1/metrics,对"只给 base"的 traces 值 Replace 不生效、恰好被随后的 resolveEndpoint 补上(逻辑侥幸自洽),但对形如 .../v1/traces/ 之外的第三种写法(如 base 已带 /api/default/v1)推导结果不可预期。OTEL_EXPORTER_OTLP_ENDPOINT 未被识别:运维只配这一个(最普遍的做法)时,观测静默全关(叠加 H4 的无日志问题更难排查)。net/url 解析后按 path 判断并拼接;支持 OTEL_EXPORTER_OTLP_ENDPOINT 作为 base 回退;单元测试覆盖「base / 全路径 / 带尾斜杠 / 带查询串 / 仅 metrics」5 种组合。calc_round_duration_ms 把单位写进指标名,且创建时未使用 metric.WithUnit("ms")(约定应命名 calc_round_duration + unit=ms);calc.round span(L173)未 SetAttributes(result/成功数/失败数)也未 SetStatus,出错时链路里看不出红点,可观测性收益有限;span 以 context.Background() 起始,不继承网关 traceparent(后台轮询场景可接受,但与包文档中"traceparent 继承"表述无关,勿混淆)。WithUnit("ms");在 defer 里给 span 补 result、success_count、fail_count 属性与错误状态。internal/obs/ 下仅有 obs.go,无 obs_test.go;而仓库其余包(如 internal/calculator 的 outlier_test.go、pipeline_test.go)均配套测试。resolveEndpoint 归一化)是本包最容易出错的逻辑,恰恰没有任何测试固化。resolveEndpoint 的表驱动测试与「未配置 → noop 且不 panic」的测试(纯函数/无网络,成本极低)。go.mod 一次性引入 7 个 OTel 直接依赖(v1.46.0)并连带 grpc / genproto / protobuf / x/net / x/crypto 等间接依赖;go.sum 中同时出现 go.opentelemetry.io/otel/metric/x v0.68.0、go.yaml.in/yaml/v3 v3.0.5 这类不常见模块路径,并删除了 davecgh/go-spew、pmezard/go-difflib、gopkg.in/yaml.v3 的部分 h1: 校验行(随 stretchr/testify 由 v1.10.0 升至 v1.12.1)。missing go.sum entry 构建失败。go mod verify && go build ./... && go test ./...;确认 otel/metric/x、go.yaml.in/yaml/v3 是否为预期的传递依赖(供应链核对),并确认 testify 升级是本次有意为之而非 go mod tidy 副作用。结论:v1.0.25 是一次低风险的功能新增(新增 internal/obs 观测包 + 两处接线),fail-open 的工程约定实现得比较克制、不会影响业务链路,未引入业务回归;本版本修复 0 条历史问题,新增 3 条结构性问题(1 Medium 高耦合 + 2 Low,全部集中在新增文件 obs.go)。
真正需要关注的不是工具报的 3 条结构问题,而是两条"观测数据本身不可信"的问题:
calc_commodity_total 把 (品类×日期) 对数当品类数上报 → 指标系统性虚高。result=ok → 该观测体系恰好无法发现它最该发现的异常。这两条会让"引入观测"的目标落空,建议在下一个 patch 版本内修掉。
合并前建议动作(按优先级):
| 优先级 | 动作 |
|---|---|
| P0 | CI 跑 go mod verify && go build ./... && go test ./...,确认 go.sum 删除校验行未破坏构建(H8) |
| P0 | 修 H2:错误早退分支置 roundResult="error";部分失败单列 result |
| P1 | 修 H1:calc_commodity_total 改名或改传真实品类数 |
| P1 | 补 H7:resolveEndpoint 表驱动测试 + 未配置 noop 测试 |
| P2 | 落实工具 3 条:拆分 Init 为 initTracing/initMetrics/newResource(同时解决高耦合与扇出异常)、抽取重复的导出器构造块 |
| P2 | 修 H3(接线或删除死指标)、H4(删死分支 + noop 打日志)、H5(net/url 归一化 + 支持通用 base 变量)、H6(WithUnit + span 属性/状态) |
验证边界声明:本报告基于静态质检数据与源码阅读,环境内无法执行 go build / go test / 运行时联调,因此 H8 中"go.sum 完整性 / 依赖必要性"以及"实际能否成功导出到 OpenObserve"两项结论需由 CI 与联调环境最终确认。
runCalculationCycle 入口开 span、defer 记录本轮 result(ok|skipped|error)、耗时与计数。