From 5532c96c44bf56d1ddcdbc2964c9c0772e3060d2 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 10 Sep 2026 16:25:13 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20CLAUDE.md?= =?UTF-8?q?=EF=BC=88=E4=BB=93=E5=BA=93=E5=AE=9A=E4=BD=8D=20+=20=E5=9B=9B?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=97=A5=E5=BF=97/=E6=8C=87=E6=A0=87?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=8C=87=E5=BC=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 给 Claude Code 在仓库内的作业指引,也是新人接手的速览: - 铁律:spec/ 是唯一事实来源、薄封装不自研 instrumentation、首期只做 log+metrics(trace_id/span_id 仅预留)、契约改动四语言同步、日志只支持 kv 传参禁止 printf、高基数值禁做 metrics label、community 只在可信判定点解析。 - 契约速查:字段顺序、time 固定毫秒 UTC、level 小写、logger/error 语义、 community 双层注入与取值来源。 - 四语言各自的「加日志 / 加指标」代码块(Init + 打点 + 中间件 + 请求级覆盖), 签名均按各语言实际源码核对。 - 写明 Java 日志必须用 examples/logback-json.xml 的 ObsJsonProvider 及原因, 避免后人退回 encoder 自带 provider(会丢 throwable、字段名/级别不符)。 - 构建测试命令与 CI 工具链版本(Go 取 go.mod、Python 3.10、Node 20、Java 17); 不含任何本机绝对路径,便于团队共享。 注意:文中 kv-only 日志与 Java ObsJsonProvider 描述的是 PR #4 合入后的状态。 Co-Authored-By: Claude Code --- CLAUDE.md | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..34f8b79 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,187 @@ +# CLAUDE.md + +本文件给 Claude Code 在本仓库工作时使用。人类读者请看 [README.md](README.md) 与各语言 README。 + +## 仓库是什么 + +opensourceways 微服务的**可观测薄封装 SDK monorepo**:把「结构化 JSON 日志 + Prometheus 指标」两个能力, +按各语言官方库做一层装配(中间件 / 通用字段注入 / 命名对齐),**不自研 instrumentation**。 +输出格式由 [spec/](spec/README.md) 契约层统一约束,四个语言实现必须对齐。 + +需求:[backlog#1938](https://github.com/opensourceways/backlog/issues/1938) · 子任务:[backlog#2061](https://github.com/opensourceways/backlog/issues/2061) + +## 铁律(改代码前必读) + +1. **`spec/` 是唯一事实来源**。涉及日志/指标字段名、取值、命名的改动,先改 spec,再改四个语言实现。实现与 spec 不一致时以 spec 为准。 +2. **薄封装,不自研 instrumentation**。指标底层一律引用官方库:Go→`client_golang`、Python→`prometheus-client`、Java→`micrometer`+Actuator、Node→`prom-client`。SDK 只做装配。 +3. **首期只做 log + metrics,不含 trace**。`trace_id` / `span_id` 是**预留注入位**:字段可写可透传、有值才输出,首期不落 span。改动时不要顺手"补全"成真 trace。 +4. **契约改动必须四语言同步**。改一个字段就要动 `spec/` + `go/` `python/` `node/` `java/`,不要只改一处。社区枚举(`community`)例外——它只是文档,随上游刷新即可。 +5. **日志只支持 kv 传参,禁止 printf 风格**。`msg` 必须是常量短语,可变数据走键值对。理由:msg 内嵌值会让每行都不同,无法聚合计数,告警规则会静默失效。 +6. **高基数值禁止当 metrics label**(`request_id` / `trace_id` / `span_id` 等),会撑爆时序基数。 +7. **`community` 只在可信判定点解析**(路由前缀 / 认证主体 / 白名单),**禁止裸读 URL / Header**。 + +## 目录结构 + +``` +spec/ 契约层:log-format / metrics-format / common-fields / community-values(唯一权威定义) +go/ obs-sdk-go —— log / metrics / sdkctx / middleware(+ginmw) +python/ obs-sdk-python —— obs_sdk/{log,metrics,_context,middleware} +node/ obs-sdk-node —— lib/{log,metrics,context,middleware} +java/ obs-sdk-java —— io.opensourceways.obssdk.{ObsSdkConfig,ObsMetrics,log,context,middleware} +``` + +各语言子目录自管版本(`go.mod` / `pyproject.toml` / `pom.xml` / `package.json`),Git tag 用 `go-v1.0.0` 等前缀区分。 + +## 契约速查 + +**日志字段顺序**:`time / level / msg / service / env / instance / community / request_id / trace_id / span_id / logger / error` + 业务字段(扁平,`snake_case`)。 +Go 与 Java 按此顺序输出;Python / Node 业务字段在前,但字段名与取值一致。 + +**`time`**:固定毫秒精度(3 位小数)UTC,以 `Z` 结尾(**不是** `+00:00`,**不是**纳秒)。 +**`level`**:小写 `debug` / `info` / `warn` / `error`。 +**`logger`**:调用位置 `文件:行号`(调试定位用)。Go/Java 输出,Python/Node 暂不输出(契约中该字段可选)。 +**`error`**:错误信息。Go 输出 `err.Error()` 文本(低基数);Python 等有异常上下文的语言**可含完整 traceback**(多行,JSON 转义为 `\n`,仍是单行 JSON)。**该字段不适合聚合**(取值逐次不同),按错误类型聚合请用 `msg` 常量 + 业务字段。 + +**community 双层注入**(四种语言同一语义,贯穿日志与指标): +- `service` / `env` / `instance`:**部署级** const,来自 Init 配置或 `OBS_SERVICE` / `OBS_ENV` / `OBS_INSTANCE` / `OBS_COMMUNITY` 环境变量。 +- `community`:普通**可变** label/字段 —— 请求上下文覆盖优先,未覆盖回退部署默认。中心化多社区服务靠它按请求区分。 +- `community` 取值枚举见 [spec/community-values.md](spec/community-values.md),来源是 `opensourceways/infrastructure` 仓的 `service.yaml`(不是值就先去那里查,别自己编)。 + +## 各语言:怎么加日志与指标 + +### Go(`go/`) + +```go +import ( + obslog "github.com/opensourceways/obs-sdk/go/log" + obsmetrics "github.com/opensourceways/obs-sdk/go/metrics" + obshttpmw "github.com/opensourceways/obs-sdk/go/middleware" + "github.com/opensourceways/obs-sdk/go/sdkctx" +) + +// 启动时 Init 一次,之后全进程用包级函数(kratos v3 形状,无需在每个调用点绑定 logger) +obslog.Init(obslog.Config{Service: "review", Env: "test", Instance: "pod-1", Community: "openEuler"}) + +// 日志:msg 常量 + kv 交替(禁止 printf) +obslog.Info("job done", "event", "release", "issue", "2061") +obslog.ErrorContext(ctx, "get account failed", "user_id", uid, "error", err) // ctx 里的请求字段自动附加 + +// 指标:注册一次;community label 自动排首位,取请求覆盖或回退默认 +m := obsmetrics.New(obsmetrics.Config{Service: "review", Env: "test", Instance: "pod-1", Community: "openEuler"}) +built := m.NewCounterVec("built_releases", "发布的构建数", "kind") +built.Inc("tag") +built.IncWithContext(ctx, "tag") // 请求内 → 该条 series 的 community 取 ctx 覆盖值 + +// 中间件:注入 request_id + 可信判定点解析 community + 记 obs_http_server_* +h := obshttpmw.New(obshttpmw.Options{ + Metrics: m, + ResolveCommunity: func(r *http.Request) string { /* "/mindspore" → "mindspore" */ return "" }, +}).Then(myHandler) +// gin:r.Use(ginmw.Middleware(ginmw.Options{Metrics: m})) + +// 请求内覆盖(trace_id/span_id 为二期预留) +ctx = sdkctx.WithCommunity(ctx, "mindspore") +ctx = sdkctx.WithRequestID(ctx, "req-123") +``` + +指标注册:`NewCounterVec` / `NewGaugeVec` / `NewHistogramVec` / `NewHistogramVecWithBuckets`,名称为**基础名**(不带 `_total` / `_seconds`);暴露用 `m.Handler()`。 + +### Python(`python/`) + +```python +from obs_sdk import log, metrics +from obs_sdk import _context + +log.init(service="review", env="test", instance="pod-1", community="openEuler") # 进程内幂等 +logger = log.get_logger(__name__) +logger.info("job done", extra={"event": "release", "issue": "2061"}) # 业务字段走 extra= +log.info("job done", extra={"issue": "2061"}) # 或便捷函数 + +metrics.init(service="review", env="test", instance="pod-1", community="openEuler") +built = metrics.counter("built_releases", "发布的构建数", ["kind"]) # community 自动补 +built.inc(1, kind="tag") + +# 请求上下文:中间件之外也可手工 bind(community 必须在可信判定点解析) +with _context.bind(community="mindspore", request_id="req-1"): + logger.info("scoped") # 自动带 community/request_id + built.inc(1, kind="tag") # 该 series community 被覆盖 + +# /metrics 暴露 +# return Response(content=metrics.generate_text(), media_type=metrics.content_type()) +``` + +框架适配:`middleware.fastapi_wrap(app, resolver=...)`、`flask_middleware(app, resolver=...)`、`DjangoMiddleware`(MIDDLEWARE 列表加 `obs_sdk.middleware.DjangoMiddleware`)。 +多注册表场景直接 `metrics.Metrics(...)` 而非模块级单例。 + +### Node(`node/`) + +```js +const obs = require('obs-sdk-node'); // { log, metrics, context, middleware } + +obs.log.init({ service: 'review', env: 'test', instance: 'pod-1', community: 'openEuler' }); +obs.log.info('job done', { event: 'release', issue: '2061' }); // 业务字段走第二个对象参数 + +const m = new obs.metrics.Metrics({ service: 'review', env: 'test', instance: 'pod-1', community: 'openEuler' }); +const built = m.counter('built_releases_total', '发布的构建数', ['kind']); // prom-client 要最终名,SDK 不改名 +built.inc(1, { kind: 'tag' }); + +// 中间件:注入 request_id + 解析 community + 记 obs_http_server_* +const { makeMiddleware, metricsRouteHandler } = obs.middleware; +app.use(makeMiddleware({ metrics: m, resolveCommunity: (req) => undefined })); +app.get('/metrics', metricsRouteHandler(m)); + +// 请求内覆盖 +obs.context.bindRequest({ community: 'mindspore', requestId: 'req-1' }, () => { + obs.log.info('scoped'); +}); +``` + +### Java(`java/`) + +```java +ObsSdkConfig cfg = ObsSdkConfig.builder() + .service("review").env("test").instance("pod-1").community("openEuler") + .build(); // 生产用 ObsSdkConfig.fromEnvironment() 读 OBS_* + +ObsLogging.init(cfg); // 部署级字段写入 MDC +ObsMetrics m = ObsMetrics.of(cfg); + +ObsMetrics.CounterVec built = m.counter("built_releases", "发布的构建数", "kind"); +built.inc("tag"); // 基础名(不带 _total/_seconds),Micrometer 自动补后缀 + +// 请求处理:可信判定点解析后 push(try-with-resources) +try (RequestContext.Scope scope = RequestContext.push("mindspore", "req-1", null)) { + log.info("job done"); // SLF4J;MDC 里的请求字段由 JSON encoder 输出 + built.inc("tag"); // 该 series community 取覆盖值 +} +``` + +日志 JSON 输出**必须**用 [java/examples/logback-json.xml](java/examples/logback-json.xml):它只挂 SDK 的 +`ObsJsonProvider`,由 SDK 保证字段名/顺序/时间格式/级别小写/异常堆栈。**不要退回 encoder 自带 provider**—— +`` 只能输出大写、字段名不可配、且不配 `` 会整条丢弃 throwable。 +该 provider 需要 `logstash-logback-encoder` + `jackson-core`(SDK 内为 `provided`,接入服务运行时提供)。 + +Servlet 接入(可选):`new ObsFilter(req -> resolveCommunity(req))`,或 Spring Boot 注册 `FilterRegistrationBean`。 +Java 的**服务端指标**不重复埋点——走 Spring Boot Actuator + Micrometer 官方 server instrumentation,把 +`m.meterRegistry()` 暴露成 bean 由 Actuator 托管。 + +## 构建与测试 + +```bash +cd go && go vet ./... && go test -race ./... +cd python && pytest # 需 .venv 内已装 prometheus_client 等依赖 +cd node && npm install && npm test +cd java && mvn test # 需 JDK 17 + Maven +``` + +工具链版本对齐 CI([.github/workflows/ci.yml](.github/workflows/ci.yml)):Go 取 `go/go.mod` 的版本(1.22)、Python 3.10、Node 20、Java 17。 +本机没有系统级 JDK/Maven 时,可把 `JAVA_HOME` / `PATH` 指向自装工具链;Java 的最终验证以 CI 为准。 + +## 改动约定 + +- **改了契约就四语言同步**,并在各语言补 UT。断言要落在**真实输出**上,不要只断言 MDC / 中间变量—— + Java 的字段名/级别/时区问题(以及 throwable 被丢弃)正是因为只测了 MDC 才漏到合入后的。 +- Java 的 `ObsJsonProviderTest` 会**直接加载 `examples/logback-json.xml`**(测试工作目录是 `java/`), + 改动样例配置会反映到测试里。 +- 新增/改动 `community` 取值前先同步 [spec/community-values.md](spec/community-values.md),其来源与重新同步命令见该文件。 +- 发现代码里的 bug:**指出来,但不要顺手修**(超出当前任务范围的改动先问)。 From 9a1c46b5585118bfb8f24de4a9c20c455a1a0564 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 10 Sep 2026 16:30:00 +0800 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20CLAUDE.md=20=E7=9A=84=20Go=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=B8=8D=E7=A1=AC=E7=BC=96=E7=A0=81=EF=BC=88?= =?UTF-8?q?=E4=BB=A5=20go/go.mod=20=E4=B8=BA=E5=87=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原写「Go 取 go/go.mod 的版本(1.22)」,但 #3 已把 go.mod 升到 1.25.0, 括号里的数字当场过期。改为只说明来源(CI 用 go-version-file 读 go.mod), 避免每次升 Go 都要回来改文档。 Co-Authored-By: Claude Code --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 34f8b79..f2cb1dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,7 +174,8 @@ cd node && npm install && npm test cd java && mvn test # 需 JDK 17 + Maven ``` -工具链版本对齐 CI([.github/workflows/ci.yml](.github/workflows/ci.yml)):Go 取 `go/go.mod` 的版本(1.22)、Python 3.10、Node 20、Java 17。 +工具链版本对齐 CI([.github/workflows/ci.yml](.github/workflows/ci.yml)):**Go 的版本以 `go/go.mod` 的 `go` 指令为准** +(CI 用 `go-version-file` 读它,不要在文档里硬编码具体版本)、Python 3.10、Node 20、Java 17。 本机没有系统级 JDK/Maven 时,可把 `JAVA_HOME` / `PATH` 指向自装工具链;Java 的最终验证以 CI 为准。 ## 改动约定 From 4ee1e5f4e8c760dc9d8203c19ff88fd4b38966e4 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 10 Sep 2026 17:34:22 +0800 Subject: [PATCH 3/6] =?UTF-8?q?docs:=20=E6=8C=87=E5=BC=95=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=94=B9=E4=B8=BA=20AGENTS.md=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E7=BB=91=E5=AE=9A=E5=8D=95=E4=B8=80=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md 是跨工具约定(opencode / Codex / Cursor 等均读它),open 且不绑定厂商; opencode 官方规则文档明确:同目录下 AGENTS.md 与 CLAUDE.md 并存时只读 AGENTS.md。 故把内容迁到 AGENTS.md(git mv 保留历史),措辞改为「AI coding agent 通用指引」。 Claude Code 不原生读 AGENTS.md(官方 memory 文档:Claude Code reads CLAUDE.md, not AGENTS.md,原生支持仍是 open feature request),官方给的兼容方式是 @AGENTS.md 导入或符号链接。这里用导入 —— 符号链接在 Windows 需管理员/开发者模式,本仓库贡献者 可能用 Windows,导入没有这个约束。 CLAUDE.md 因此退化为 4 行导入壳,正文只在 AGENTS.md 一处维护,不会两处漂移。 同时按反馈去掉「子任务 #2061」引用,只保留需求 #1938。 Co-Authored-By: Claude Code --- AGENTS.md | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 188 +---------------------------------------------------- 2 files changed, 192 insertions(+), 185 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..19baefd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,189 @@ +# AGENTS.md + +本文件是给 AI coding agent(Claude Code / opencode / Codex 等)在本仓库工作时的通用指引, +不绑定任何单一工具。人类读者请看 [README.md](README.md) 与各语言 README。 + +## 仓库是什么 + +opensourceways 微服务的**可观测薄封装 SDK monorepo**:把「结构化 JSON 日志 + Prometheus 指标」两个能力, +按各语言官方库做一层装配(中间件 / 通用字段注入 / 命名对齐),**不自研 instrumentation**。 +输出格式由 [spec/](spec/README.md) 契约层统一约束,四个语言实现必须对齐。 + +需求:[backlog#1938](https://github.com/opensourceways/backlog/issues/1938) + +## 铁律(改代码前必读) + +1. **`spec/` 是唯一事实来源**。涉及日志/指标字段名、取值、命名的改动,先改 spec,再改四个语言实现。实现与 spec 不一致时以 spec 为准。 +2. **薄封装,不自研 instrumentation**。指标底层一律引用官方库:Go→`client_golang`、Python→`prometheus-client`、Java→`micrometer`+Actuator、Node→`prom-client`。SDK 只做装配。 +3. **首期只做 log + metrics,不含 trace**。`trace_id` / `span_id` 是**预留注入位**:字段可写可透传、有值才输出,首期不落 span。改动时不要顺手"补全"成真 trace。 +4. **契约改动必须四语言同步**。改一个字段就要动 `spec/` + `go/` `python/` `node/` `java/`,不要只改一处。社区枚举(`community`)例外——它只是文档,随上游刷新即可。 +5. **日志只支持 kv 传参,禁止 printf 风格**。`msg` 必须是常量短语,可变数据走键值对。理由:msg 内嵌值会让每行都不同,无法聚合计数,告警规则会静默失效。 +6. **高基数值禁止当 metrics label**(`request_id` / `trace_id` / `span_id` 等),会撑爆时序基数。 +7. **`community` 只在可信判定点解析**(路由前缀 / 认证主体 / 白名单),**禁止裸读 URL / Header**。 + +## 目录结构 + +``` +spec/ 契约层:log-format / metrics-format / common-fields / community-values(唯一权威定义) +go/ obs-sdk-go —— log / metrics / sdkctx / middleware(+ginmw) +python/ obs-sdk-python —— obs_sdk/{log,metrics,_context,middleware} +node/ obs-sdk-node —— lib/{log,metrics,context,middleware} +java/ obs-sdk-java —— io.opensourceways.obssdk.{ObsSdkConfig,ObsMetrics,log,context,middleware} +``` + +各语言子目录自管版本(`go.mod` / `pyproject.toml` / `pom.xml` / `package.json`),Git tag 用 `go-v1.0.0` 等前缀区分。 + +## 契约速查 + +**日志字段顺序**:`time / level / msg / service / env / instance / community / request_id / trace_id / span_id / logger / error` + 业务字段(扁平,`snake_case`)。 +Go 与 Java 按此顺序输出;Python / Node 业务字段在前,但字段名与取值一致。 + +**`time`**:固定毫秒精度(3 位小数)UTC,以 `Z` 结尾(**不是** `+00:00`,**不是**纳秒)。 +**`level`**:小写 `debug` / `info` / `warn` / `error`。 +**`logger`**:调用位置 `文件:行号`(调试定位用)。Go/Java 输出,Python/Node 暂不输出(契约中该字段可选)。 +**`error`**:错误信息。Go 输出 `err.Error()` 文本(低基数);Python 等有异常上下文的语言**可含完整 traceback**(多行,JSON 转义为 `\n`,仍是单行 JSON)。**该字段不适合聚合**(取值逐次不同),按错误类型聚合请用 `msg` 常量 + 业务字段。 + +**community 双层注入**(四种语言同一语义,贯穿日志与指标): +- `service` / `env` / `instance`:**部署级** const,来自 Init 配置或 `OBS_SERVICE` / `OBS_ENV` / `OBS_INSTANCE` / `OBS_COMMUNITY` 环境变量。 +- `community`:普通**可变** label/字段 —— 请求上下文覆盖优先,未覆盖回退部署默认。中心化多社区服务靠它按请求区分。 +- `community` 取值枚举见 [spec/community-values.md](spec/community-values.md),来源是 `opensourceways/infrastructure` 仓的 `service.yaml`(不是值就先去那里查,别自己编)。 + +## 各语言:怎么加日志与指标 + +### Go(`go/`) + +```go +import ( + obslog "github.com/opensourceways/obs-sdk/go/log" + obsmetrics "github.com/opensourceways/obs-sdk/go/metrics" + obshttpmw "github.com/opensourceways/obs-sdk/go/middleware" + "github.com/opensourceways/obs-sdk/go/sdkctx" +) + +// 启动时 Init 一次,之后全进程用包级函数(kratos v3 形状,无需在每个调用点绑定 logger) +obslog.Init(obslog.Config{Service: "review", Env: "test", Instance: "pod-1", Community: "openEuler"}) + +// 日志:msg 常量 + kv 交替(禁止 printf) +obslog.Info("job done", "event", "release", "issue", "2061") +obslog.ErrorContext(ctx, "get account failed", "user_id", uid, "error", err) // ctx 里的请求字段自动附加 + +// 指标:注册一次;community label 自动排首位,取请求覆盖或回退默认 +m := obsmetrics.New(obsmetrics.Config{Service: "review", Env: "test", Instance: "pod-1", Community: "openEuler"}) +built := m.NewCounterVec("built_releases", "发布的构建数", "kind") +built.Inc("tag") +built.IncWithContext(ctx, "tag") // 请求内 → 该条 series 的 community 取 ctx 覆盖值 + +// 中间件:注入 request_id + 可信判定点解析 community + 记 obs_http_server_* +h := obshttpmw.New(obshttpmw.Options{ + Metrics: m, + ResolveCommunity: func(r *http.Request) string { /* "/mindspore" → "mindspore" */ return "" }, +}).Then(myHandler) +// gin:r.Use(ginmw.Middleware(ginmw.Options{Metrics: m})) + +// 请求内覆盖(trace_id/span_id 为二期预留) +ctx = sdkctx.WithCommunity(ctx, "mindspore") +ctx = sdkctx.WithRequestID(ctx, "req-123") +``` + +指标注册:`NewCounterVec` / `NewGaugeVec` / `NewHistogramVec` / `NewHistogramVecWithBuckets`,名称为**基础名**(不带 `_total` / `_seconds`);暴露用 `m.Handler()`。 + +### Python(`python/`) + +```python +from obs_sdk import log, metrics +from obs_sdk import _context + +log.init(service="review", env="test", instance="pod-1", community="openEuler") # 进程内幂等 +logger = log.get_logger(__name__) +logger.info("job done", extra={"event": "release", "issue": "2061"}) # 业务字段走 extra= +log.info("job done", extra={"issue": "2061"}) # 或便捷函数 + +metrics.init(service="review", env="test", instance="pod-1", community="openEuler") +built = metrics.counter("built_releases", "发布的构建数", ["kind"]) # community 自动补 +built.inc(1, kind="tag") + +# 请求上下文:中间件之外也可手工 bind(community 必须在可信判定点解析) +with _context.bind(community="mindspore", request_id="req-1"): + logger.info("scoped") # 自动带 community/request_id + built.inc(1, kind="tag") # 该 series community 被覆盖 + +# /metrics 暴露 +# return Response(content=metrics.generate_text(), media_type=metrics.content_type()) +``` + +框架适配:`middleware.fastapi_wrap(app, resolver=...)`、`flask_middleware(app, resolver=...)`、`DjangoMiddleware`(MIDDLEWARE 列表加 `obs_sdk.middleware.DjangoMiddleware`)。 +多注册表场景直接 `metrics.Metrics(...)` 而非模块级单例。 + +### Node(`node/`) + +```js +const obs = require('obs-sdk-node'); // { log, metrics, context, middleware } + +obs.log.init({ service: 'review', env: 'test', instance: 'pod-1', community: 'openEuler' }); +obs.log.info('job done', { event: 'release', issue: '2061' }); // 业务字段走第二个对象参数 + +const m = new obs.metrics.Metrics({ service: 'review', env: 'test', instance: 'pod-1', community: 'openEuler' }); +const built = m.counter('built_releases_total', '发布的构建数', ['kind']); // prom-client 要最终名,SDK 不改名 +built.inc(1, { kind: 'tag' }); + +// 中间件:注入 request_id + 解析 community + 记 obs_http_server_* +const { makeMiddleware, metricsRouteHandler } = obs.middleware; +app.use(makeMiddleware({ metrics: m, resolveCommunity: (req) => undefined })); +app.get('/metrics', metricsRouteHandler(m)); + +// 请求内覆盖 +obs.context.bindRequest({ community: 'mindspore', requestId: 'req-1' }, () => { + obs.log.info('scoped'); +}); +``` + +### Java(`java/`) + +```java +ObsSdkConfig cfg = ObsSdkConfig.builder() + .service("review").env("test").instance("pod-1").community("openEuler") + .build(); // 生产用 ObsSdkConfig.fromEnvironment() 读 OBS_* + +ObsLogging.init(cfg); // 部署级字段写入 MDC +ObsMetrics m = ObsMetrics.of(cfg); + +ObsMetrics.CounterVec built = m.counter("built_releases", "发布的构建数", "kind"); +built.inc("tag"); // 基础名(不带 _total/_seconds),Micrometer 自动补后缀 + +// 请求处理:可信判定点解析后 push(try-with-resources) +try (RequestContext.Scope scope = RequestContext.push("mindspore", "req-1", null)) { + log.info("job done"); // SLF4J;MDC 里的请求字段由 JSON encoder 输出 + built.inc("tag"); // 该 series community 取覆盖值 +} +``` + +日志 JSON 输出**必须**用 [java/examples/logback-json.xml](java/examples/logback-json.xml):它只挂 SDK 的 +`ObsJsonProvider`,由 SDK 保证字段名/顺序/时间格式/级别小写/异常堆栈。**不要退回 encoder 自带 provider**—— +`` 只能输出大写、字段名不可配、且不配 `` 会整条丢弃 throwable。 +该 provider 需要 `logstash-logback-encoder` + `jackson-core`(SDK 内为 `provided`,接入服务运行时提供)。 + +Servlet 接入(可选):`new ObsFilter(req -> resolveCommunity(req))`,或 Spring Boot 注册 `FilterRegistrationBean`。 +Java 的**服务端指标**不重复埋点——走 Spring Boot Actuator + Micrometer 官方 server instrumentation,把 +`m.meterRegistry()` 暴露成 bean 由 Actuator 托管。 + +## 构建与测试 + +```bash +cd go && go vet ./... && go test -race ./... +cd python && pytest # 需 .venv 内已装 prometheus_client 等依赖 +cd node && npm install && npm test +cd java && mvn test # 需 JDK 17 + Maven +``` + +工具链版本对齐 CI([.github/workflows/ci.yml](.github/workflows/ci.yml)):**Go 的版本以 `go/go.mod` 的 `go` 指令为准** +(CI 用 `go-version-file` 读它,不要在文档里硬编码具体版本)、Python 3.10、Node 20、Java 17。 +本机没有系统级 JDK/Maven 时,可把 `JAVA_HOME` / `PATH` 指向自装工具链;Java 的最终验证以 CI 为准。 + +## 改动约定 + +- **改了契约就四语言同步**,并在各语言补 UT。断言要落在**真实输出**上,不要只断言 MDC / 中间变量—— + Java 的字段名/级别/时区问题(以及 throwable 被丢弃)正是因为只测了 MDC 才漏到合入后的。 +- Java 的 `ObsJsonProviderTest` 会**直接加载 `examples/logback-json.xml`**(测试工作目录是 `java/`), + 改动样例配置会反映到测试里。 +- 新增/改动 `community` 取值前先同步 [spec/community-values.md](spec/community-values.md),其来源与重新同步命令见该文件。 +- 发现代码里的 bug:**指出来,但不要顺手修**(超出当前任务范围的改动先问)。 diff --git a/CLAUDE.md b/CLAUDE.md index f2cb1dc..ad24891 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,188 +1,6 @@ # CLAUDE.md -本文件给 Claude Code 在本仓库工作时使用。人类读者请看 [README.md](README.md) 与各语言 README。 +本仓库的 agent 指引统一维护在 [AGENTS.md](AGENTS.md)(跨工具通用,Claude Code / opencode / Codex 等均适用)。 +此处只做导入,避免同一份内容两处维护。 -## 仓库是什么 - -opensourceways 微服务的**可观测薄封装 SDK monorepo**:把「结构化 JSON 日志 + Prometheus 指标」两个能力, -按各语言官方库做一层装配(中间件 / 通用字段注入 / 命名对齐),**不自研 instrumentation**。 -输出格式由 [spec/](spec/README.md) 契约层统一约束,四个语言实现必须对齐。 - -需求:[backlog#1938](https://github.com/opensourceways/backlog/issues/1938) · 子任务:[backlog#2061](https://github.com/opensourceways/backlog/issues/2061) - -## 铁律(改代码前必读) - -1. **`spec/` 是唯一事实来源**。涉及日志/指标字段名、取值、命名的改动,先改 spec,再改四个语言实现。实现与 spec 不一致时以 spec 为准。 -2. **薄封装,不自研 instrumentation**。指标底层一律引用官方库:Go→`client_golang`、Python→`prometheus-client`、Java→`micrometer`+Actuator、Node→`prom-client`。SDK 只做装配。 -3. **首期只做 log + metrics,不含 trace**。`trace_id` / `span_id` 是**预留注入位**:字段可写可透传、有值才输出,首期不落 span。改动时不要顺手"补全"成真 trace。 -4. **契约改动必须四语言同步**。改一个字段就要动 `spec/` + `go/` `python/` `node/` `java/`,不要只改一处。社区枚举(`community`)例外——它只是文档,随上游刷新即可。 -5. **日志只支持 kv 传参,禁止 printf 风格**。`msg` 必须是常量短语,可变数据走键值对。理由:msg 内嵌值会让每行都不同,无法聚合计数,告警规则会静默失效。 -6. **高基数值禁止当 metrics label**(`request_id` / `trace_id` / `span_id` 等),会撑爆时序基数。 -7. **`community` 只在可信判定点解析**(路由前缀 / 认证主体 / 白名单),**禁止裸读 URL / Header**。 - -## 目录结构 - -``` -spec/ 契约层:log-format / metrics-format / common-fields / community-values(唯一权威定义) -go/ obs-sdk-go —— log / metrics / sdkctx / middleware(+ginmw) -python/ obs-sdk-python —— obs_sdk/{log,metrics,_context,middleware} -node/ obs-sdk-node —— lib/{log,metrics,context,middleware} -java/ obs-sdk-java —— io.opensourceways.obssdk.{ObsSdkConfig,ObsMetrics,log,context,middleware} -``` - -各语言子目录自管版本(`go.mod` / `pyproject.toml` / `pom.xml` / `package.json`),Git tag 用 `go-v1.0.0` 等前缀区分。 - -## 契约速查 - -**日志字段顺序**:`time / level / msg / service / env / instance / community / request_id / trace_id / span_id / logger / error` + 业务字段(扁平,`snake_case`)。 -Go 与 Java 按此顺序输出;Python / Node 业务字段在前,但字段名与取值一致。 - -**`time`**:固定毫秒精度(3 位小数)UTC,以 `Z` 结尾(**不是** `+00:00`,**不是**纳秒)。 -**`level`**:小写 `debug` / `info` / `warn` / `error`。 -**`logger`**:调用位置 `文件:行号`(调试定位用)。Go/Java 输出,Python/Node 暂不输出(契约中该字段可选)。 -**`error`**:错误信息。Go 输出 `err.Error()` 文本(低基数);Python 等有异常上下文的语言**可含完整 traceback**(多行,JSON 转义为 `\n`,仍是单行 JSON)。**该字段不适合聚合**(取值逐次不同),按错误类型聚合请用 `msg` 常量 + 业务字段。 - -**community 双层注入**(四种语言同一语义,贯穿日志与指标): -- `service` / `env` / `instance`:**部署级** const,来自 Init 配置或 `OBS_SERVICE` / `OBS_ENV` / `OBS_INSTANCE` / `OBS_COMMUNITY` 环境变量。 -- `community`:普通**可变** label/字段 —— 请求上下文覆盖优先,未覆盖回退部署默认。中心化多社区服务靠它按请求区分。 -- `community` 取值枚举见 [spec/community-values.md](spec/community-values.md),来源是 `opensourceways/infrastructure` 仓的 `service.yaml`(不是值就先去那里查,别自己编)。 - -## 各语言:怎么加日志与指标 - -### Go(`go/`) - -```go -import ( - obslog "github.com/opensourceways/obs-sdk/go/log" - obsmetrics "github.com/opensourceways/obs-sdk/go/metrics" - obshttpmw "github.com/opensourceways/obs-sdk/go/middleware" - "github.com/opensourceways/obs-sdk/go/sdkctx" -) - -// 启动时 Init 一次,之后全进程用包级函数(kratos v3 形状,无需在每个调用点绑定 logger) -obslog.Init(obslog.Config{Service: "review", Env: "test", Instance: "pod-1", Community: "openEuler"}) - -// 日志:msg 常量 + kv 交替(禁止 printf) -obslog.Info("job done", "event", "release", "issue", "2061") -obslog.ErrorContext(ctx, "get account failed", "user_id", uid, "error", err) // ctx 里的请求字段自动附加 - -// 指标:注册一次;community label 自动排首位,取请求覆盖或回退默认 -m := obsmetrics.New(obsmetrics.Config{Service: "review", Env: "test", Instance: "pod-1", Community: "openEuler"}) -built := m.NewCounterVec("built_releases", "发布的构建数", "kind") -built.Inc("tag") -built.IncWithContext(ctx, "tag") // 请求内 → 该条 series 的 community 取 ctx 覆盖值 - -// 中间件:注入 request_id + 可信判定点解析 community + 记 obs_http_server_* -h := obshttpmw.New(obshttpmw.Options{ - Metrics: m, - ResolveCommunity: func(r *http.Request) string { /* "/mindspore" → "mindspore" */ return "" }, -}).Then(myHandler) -// gin:r.Use(ginmw.Middleware(ginmw.Options{Metrics: m})) - -// 请求内覆盖(trace_id/span_id 为二期预留) -ctx = sdkctx.WithCommunity(ctx, "mindspore") -ctx = sdkctx.WithRequestID(ctx, "req-123") -``` - -指标注册:`NewCounterVec` / `NewGaugeVec` / `NewHistogramVec` / `NewHistogramVecWithBuckets`,名称为**基础名**(不带 `_total` / `_seconds`);暴露用 `m.Handler()`。 - -### Python(`python/`) - -```python -from obs_sdk import log, metrics -from obs_sdk import _context - -log.init(service="review", env="test", instance="pod-1", community="openEuler") # 进程内幂等 -logger = log.get_logger(__name__) -logger.info("job done", extra={"event": "release", "issue": "2061"}) # 业务字段走 extra= -log.info("job done", extra={"issue": "2061"}) # 或便捷函数 - -metrics.init(service="review", env="test", instance="pod-1", community="openEuler") -built = metrics.counter("built_releases", "发布的构建数", ["kind"]) # community 自动补 -built.inc(1, kind="tag") - -# 请求上下文:中间件之外也可手工 bind(community 必须在可信判定点解析) -with _context.bind(community="mindspore", request_id="req-1"): - logger.info("scoped") # 自动带 community/request_id - built.inc(1, kind="tag") # 该 series community 被覆盖 - -# /metrics 暴露 -# return Response(content=metrics.generate_text(), media_type=metrics.content_type()) -``` - -框架适配:`middleware.fastapi_wrap(app, resolver=...)`、`flask_middleware(app, resolver=...)`、`DjangoMiddleware`(MIDDLEWARE 列表加 `obs_sdk.middleware.DjangoMiddleware`)。 -多注册表场景直接 `metrics.Metrics(...)` 而非模块级单例。 - -### Node(`node/`) - -```js -const obs = require('obs-sdk-node'); // { log, metrics, context, middleware } - -obs.log.init({ service: 'review', env: 'test', instance: 'pod-1', community: 'openEuler' }); -obs.log.info('job done', { event: 'release', issue: '2061' }); // 业务字段走第二个对象参数 - -const m = new obs.metrics.Metrics({ service: 'review', env: 'test', instance: 'pod-1', community: 'openEuler' }); -const built = m.counter('built_releases_total', '发布的构建数', ['kind']); // prom-client 要最终名,SDK 不改名 -built.inc(1, { kind: 'tag' }); - -// 中间件:注入 request_id + 解析 community + 记 obs_http_server_* -const { makeMiddleware, metricsRouteHandler } = obs.middleware; -app.use(makeMiddleware({ metrics: m, resolveCommunity: (req) => undefined })); -app.get('/metrics', metricsRouteHandler(m)); - -// 请求内覆盖 -obs.context.bindRequest({ community: 'mindspore', requestId: 'req-1' }, () => { - obs.log.info('scoped'); -}); -``` - -### Java(`java/`) - -```java -ObsSdkConfig cfg = ObsSdkConfig.builder() - .service("review").env("test").instance("pod-1").community("openEuler") - .build(); // 生产用 ObsSdkConfig.fromEnvironment() 读 OBS_* - -ObsLogging.init(cfg); // 部署级字段写入 MDC -ObsMetrics m = ObsMetrics.of(cfg); - -ObsMetrics.CounterVec built = m.counter("built_releases", "发布的构建数", "kind"); -built.inc("tag"); // 基础名(不带 _total/_seconds),Micrometer 自动补后缀 - -// 请求处理:可信判定点解析后 push(try-with-resources) -try (RequestContext.Scope scope = RequestContext.push("mindspore", "req-1", null)) { - log.info("job done"); // SLF4J;MDC 里的请求字段由 JSON encoder 输出 - built.inc("tag"); // 该 series community 取覆盖值 -} -``` - -日志 JSON 输出**必须**用 [java/examples/logback-json.xml](java/examples/logback-json.xml):它只挂 SDK 的 -`ObsJsonProvider`,由 SDK 保证字段名/顺序/时间格式/级别小写/异常堆栈。**不要退回 encoder 自带 provider**—— -`` 只能输出大写、字段名不可配、且不配 `` 会整条丢弃 throwable。 -该 provider 需要 `logstash-logback-encoder` + `jackson-core`(SDK 内为 `provided`,接入服务运行时提供)。 - -Servlet 接入(可选):`new ObsFilter(req -> resolveCommunity(req))`,或 Spring Boot 注册 `FilterRegistrationBean`。 -Java 的**服务端指标**不重复埋点——走 Spring Boot Actuator + Micrometer 官方 server instrumentation,把 -`m.meterRegistry()` 暴露成 bean 由 Actuator 托管。 - -## 构建与测试 - -```bash -cd go && go vet ./... && go test -race ./... -cd python && pytest # 需 .venv 内已装 prometheus_client 等依赖 -cd node && npm install && npm test -cd java && mvn test # 需 JDK 17 + Maven -``` - -工具链版本对齐 CI([.github/workflows/ci.yml](.github/workflows/ci.yml)):**Go 的版本以 `go/go.mod` 的 `go` 指令为准** -(CI 用 `go-version-file` 读它,不要在文档里硬编码具体版本)、Python 3.10、Node 20、Java 17。 -本机没有系统级 JDK/Maven 时,可把 `JAVA_HOME` / `PATH` 指向自装工具链;Java 的最终验证以 CI 为准。 - -## 改动约定 - -- **改了契约就四语言同步**,并在各语言补 UT。断言要落在**真实输出**上,不要只断言 MDC / 中间变量—— - Java 的字段名/级别/时区问题(以及 throwable 被丢弃)正是因为只测了 MDC 才漏到合入后的。 -- Java 的 `ObsJsonProviderTest` 会**直接加载 `examples/logback-json.xml`**(测试工作目录是 `java/`), - 改动样例配置会反映到测试里。 -- 新增/改动 `community` 取值前先同步 [spec/community-values.md](spec/community-values.md),其来源与重新同步命令见该文件。 -- 发现代码里的 bug:**指出来,但不要顺手修**(超出当前任务范围的改动先问)。 +@AGENTS.md From 559744b7c911dc13def79e3e17755407bc0496bf Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 10 Sep 2026 17:46:08 +0800 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20AGENTS.md=20=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E3=80=8C=E5=AD=90=E4=BB=BB=E5=8A=A1=20#2061=E3=80=8D=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前一个 commit 按反馈去掉了子任务引用,经确认应保留,恢复为与 README.md / spec/README.md 一致的写法:需求 #1938 + 子任务 #2061。 Co-Authored-By: Claude Code --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 19baefd..17ab030 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ opensourceways 微服务的**可观测薄封装 SDK monorepo**:把「结构化 按各语言官方库做一层装配(中间件 / 通用字段注入 / 命名对齐),**不自研 instrumentation**。 输出格式由 [spec/](spec/README.md) 契约层统一约束,四个语言实现必须对齐。 -需求:[backlog#1938](https://github.com/opensourceways/backlog/issues/1938) +需求:[backlog#1938](https://github.com/opensourceways/backlog/issues/1938) · 子任务:[backlog#2061](https://github.com/opensourceways/backlog/issues/2061) ## 铁律(改代码前必读) From 1cfdd0ac6e5700a1a445b8241e87537259aab173 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 10 Sep 2026 18:35:09 +0800 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20=E6=9B=B4=E6=AD=A3=20Go=20=E5=AD=90?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=A8=A1=E5=9D=97=E7=9A=84=20tag=20=E5=86=99?= =?UTF-8?q?=E6=B3=95=E4=B8=BA=20`<=E5=AD=90=E7=9B=AE=E5=BD=95>/v<=E7=89=88?= =?UTF-8?q?=E6=9C=AC>`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原文写的 `go-v1.0.0` 前缀形式 Go 工具链不认:模块根在 go/ 子目录时, tag 必须是 `go/v1.0.0`(`<子目录>/v<版本>`),否则 `go get` 拉不到该模块。 AGENTS.md 与 README.md 同步更正。 Co-Authored-By: Claude Code --- AGENTS.md | 4 +++- README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 17ab030..d54fa2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,9 @@ node/ obs-sdk-node —— lib/{log,metrics,context,middleware} java/ obs-sdk-java —— io.opensourceways.obssdk.{ObsSdkConfig,ObsMetrics,log,context,middleware} ``` -各语言子目录自管版本(`go.mod` / `pyproject.toml` / `pom.xml` / `package.json`),Git tag 用 `go-v1.0.0` 等前缀区分。 +各语言子目录自管版本(`go.mod` / `pyproject.toml` / `pom.xml` / `package.json`),Git tag 按语言加前缀区分。 +**Go 模块的 tag 必须是 `<子目录>/v<版本>` 形式**(如 `go/v1.0.0`)—— Go 工具链按模块根所在子目录解析 tag, +写成 `go-v1.0.0` 这种连字符形式 `go get` 拉不到该模块;其余语言无此约束。 ## 契约速查 diff --git a/README.md b/README.md index 70efc3c..fb05f54 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ - [java/](java/README.md) — obs-sdk-java(logback + logstash JSON encoder + micrometer/prometheus registry) - [node/](node/README.md) — obs-sdk-node(JSON serializer + prom-client + express 中间件) -各语言子目录自管版本(go.mod / pyproject.toml / pom.xml / package.json),Git tag 用 `go-v1.0.0` 等前缀区分。 +各语言子目录自管版本(go.mod / pyproject.toml / pom.xml / package.json),Git tag 按语言加前缀区分。 +Go 模块的 tag 必须是 `<子目录>/v<版本>` 形式(如 `go/v1.0.0`)—— Go 工具链按模块根所在子目录解析 tag, +写成 `go-v1.0.0` 这种连字符形式 `go get` 拉不到该模块;其余语言无此约束。 ## 通用能力(四种语言对齐) From 0a8f80d48d8e314479928064c30aa045a473adb1 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 17 Sep 2026 11:15:00 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs(agents):=20=E8=A1=A5=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E7=BA=A7=E5=AD=97=E6=AE=B5=E7=9A=84=E7=AC=AC=E4=B8=89=E7=BA=A7?= =?UTF-8?q?=E5=85=9C=E5=BA=95=E3=80=81=E6=8C=87=E5=90=91=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?/=E8=BF=9B=E5=BA=A6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 「community 双层注入」此前只写了「Init 配置或 OBS_* 环境变量」两级来源, 漏了内置默认,也没写默认值 —— 与 spec/common-fields.md「静态字段来源与默认值」 的三级解析不一致。这一句缺失有实际代价:Java SDK 的部署级字段正是因为少了一级 兜底,取值落成 null,被日志 provider 当作空值省略,导致 service/env/instance/ community 四个字段整条从 JSON 里消失、指标 label 被整段跳过,采集侧静默漏数。 补上三级来源、各语言的唯一实现位置(改兜底规则要四处同步)以及这条后果。 2. 更正 Java 片段里 `fromEnvironment()` 的注释:它也走三级解析,OBS_* 未设置时 回退内置默认,不是「只读环境变量」。 3. 补指向 docs/ 两份文档的入口:设计文档与进度追踪文档此前没有任何指引文件提及。 --- AGENTS.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d54fa2a..9e51b07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,9 @@ opensourceways 微服务的**可观测薄封装 SDK monorepo**:把「结构化 需求:[backlog#1938](https://github.com/opensourceways/backlog/issues/1938) · 子任务:[backlog#2061](https://github.com/opensourceways/backlog/issues/2061) +设计与工作量拆分见 [docs/微服务可观测性建设技术设计.md](docs/微服务可观测性建设技术设计.md), +进度与待办见 [docs/进度追踪.md](docs/进度追踪.md)。 + ## 铁律(改代码前必读) 1. **`spec/` 是唯一事实来源**。涉及日志/指标字段名、取值、命名的改动,先改 spec,再改四个语言实现。实现与 spec 不一致时以 spec 为准。 @@ -46,7 +49,13 @@ Go 与 Java 按此顺序输出;Python / Node 业务字段在前,但字段名 **`error`**:错误信息。Go 输出 `err.Error()` 文本(低基数);Python 等有异常上下文的语言**可含完整 traceback**(多行,JSON 转义为 `\n`,仍是单行 JSON)。**该字段不适合聚合**(取值逐次不同),按错误类型聚合请用 `msg` 常量 + 业务字段。 **community 双层注入**(四种语言同一语义,贯穿日志与指标): -- `service` / `env` / `instance`:**部署级** const,来自 Init 配置或 `OBS_SERVICE` / `OBS_ENV` / `OBS_INSTANCE` / `OBS_COMMUNITY` 环境变量。 +- `service` / `env` / `instance` / `community`(部署默认值)**四个部署级字段三级解析**: + **显式参数 > `OBS_SERVICE` / `OBS_ENV` / `OBS_INSTANCE` / `OBS_COMMUNITY` 环境变量 > 内置默认**。 + 内置默认是契约规定值(不是随手写的):`service` / `env` / `community` = `unknown`,`instance` = hostname。 + 各语言只有一份解析实现:Go `internal/env`、Python `obs_sdk/_env.py`、Node `lib/env.js`、Java `internal/Env` —— 改兜底规则要四处同步。 + **四个字段取值恒非空,兜底一级都不能少**:空值不会输出成 `unknown`,而是让日志 JSON 里该键**整条消失** + (provider 对空值省略该键)、指标 label 被整段跳过 —— 采集侧按字段建索引/过滤时静默漏数,且不报错。 + 少一级兜底,等价于这个字段根本没接进来。 - `community`:普通**可变** label/字段 —— 请求上下文覆盖优先,未覆盖回退部署默认。中心化多社区服务靠它按请求区分。 - `community` 取值枚举见 [spec/community-values.md](spec/community-values.md),来源是 `opensourceways/infrastructure` 仓的 `service.yaml`(不是值就先去那里查,别自己编)。 @@ -144,7 +153,8 @@ obs.context.bindRequest({ community: 'mindspore', requestId: 'req-1' }, () => { ```java ObsSdkConfig cfg = ObsSdkConfig.builder() .service("review").env("test").instance("pod-1").community("openEuler") - .build(); // 生产用 ObsSdkConfig.fromEnvironment() 读 OBS_* + .build(); // 生产也可用 ObsSdkConfig.fromEnvironment(): + // OBS_* 未设置时回退内置默认(unknown / hostname),不会为空 ObsLogging.init(cfg); // 部署级字段写入 MDC ObsMetrics m = ObsMetrics.of(cfg);