Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: install sdk
run: pip install -e python
- name: install framework + test deps
# httpx2:starlette.testclient(FastAPI 中间件单测用)的传输依赖,缺了会 RuntimeError
run: pip install 'fastapi>=0.100' 'flask>=2.0' 'django>=4.0' 'pytest>=8.0' 'httpx2>=2.0.0'
- name: install sdk + test deps
# [test] extra 含 pytest / httpx2 与三个 Web 框架。框架必须硬装:中间件单测用
# pytest.importorskip,缺框架会静默跳过 —— CI 全绿但中间件零覆盖。
run: pip install -e 'python[test]'
- name: pytest
working-directory: python
run: python -m pytest -q
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ built := m.NewCounterVec("built_releases", "发布的构建数", "kind")
built.Inc("tag")
built.IncWithContext(ctx, "tag") // 请求内 → 该条 series 的 community 取 ctx 覆盖值

// 中间件:注入 request_id + 可信判定点解析 community + 记 obs_http_server_*
// 中间件:注入 request_id + 可信判定点解析 community + 记 http_server_*
h := obshttpmw.New(obshttpmw.Options{
Metrics: m,
ResolveCommunity: func(r *http.Request) string { /* "/mindspore" → "mindspore" */ return "" },
Expand Down Expand Up @@ -137,7 +137,7 @@ const m = new obs.metrics.Metrics({ service: 'review', env: 'test', instance: 'p
const built = m.counter('built_releases_total', '发布的构建数', ['kind']); // prom-client 要最终名,SDK 不改名
built.inc(1, { kind: 'tag' });

// 中间件:注入 request_id + 解析 community + 记 obs_http_server_*
// 中间件:注入 request_id + 解析 community + 记 http_server_*
const { makeMiddleware, metricsRouteHandler } = obs.middleware;
app.use(makeMiddleware({ metrics: m, resolveCommunity: (req) => undefined }));
app.get('/metrics', metricsRouteHandler(m));
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Go 模块的 tag 必须是 `<子目录>/v<版本>` 形式(如 `go/v1.0.0`)
| community 双层注入 | `service/env/instance` 部署级 const;`community` 可变 label/字段:请求级可信判定点覆盖,未覆盖回退部署默认(`OBS_*` 环境变量) |
| 请求上下文 | Go `sdkctx`(context.Context)、Python `contextvars`、Node `AsyncLocalStorage`、Java `RequestContext`(ThreadLocal) |
| trace_id 预留 | 字段可写可透传,首期不落 span |
| 服务端指标 | Go/Python/Node 由 SDK 中间件埋 `obs_http_server_*`;Java 走 Actuator + Micrometer 官方 server instrumentation(不重复埋点) |
| 服务端指标 | Go/Python/Node 由 SDK 中间件埋 `http_server_*`;Java 走 Actuator + Micrometer 官方 server instrumentation(不重复埋点) |

## 验证

Expand Down
6 changes: 3 additions & 3 deletions docs/微服务可观测性建设技术设计.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ flowchart LR

- **命名**:全小写 `snake_case`,单位后缀遵循 Prometheus 约定(`_total` / `_seconds` / `_bytes`)。
- 业务指标强制前缀 `<service>_`(服务短名)。例:`review_http_requests_total`。
- 共享中间件公共指标用 SDK 保留前缀 **`obs_`**(不按 service 名开头——同一条 series 已带 `service` label,查询按 label 过滤)。例:`obs_http_server_requests_total`。
- 共享中间件公共指标**不加任何前缀**(不按 service 名开头,也不加 SDK 保留前缀——同一条 series 已带 `service` label,查询按 label 过滤)。例:`http_server_requests_total`。
- **通用 label 集**(每条 series 必须带):`service` / `env` / `instance` / `community`。
- 前三个是 const label;**`community` 是唯一可动态的公共 label**。
- **community 双层注入的指标实现**:对需要区分社区的指标,注册时把 `community` 声明为**普通可变 label**(service/env/instance 仍作 const label),打点时由 SDK 从上下文取覆盖值。**注册一次,单社区场景填默认值、多社区场景填覆盖值,两用。**
Expand All @@ -382,7 +382,7 @@ flowchart LR
| Init | `log.Init(cfg)` / `metrics.New(cfg)` | `log.init(...)` / `metrics.init(...)` | `ObsLogging.init(cfg)` / `ObsMetrics.of(cfg)` | `log.init(opts)` / `new Metrics(opts)` |
| 请求上下文 | `sdkctx`(`context.Context`) | `_context`(contextvars) | `RequestContext`(MDC + ThreadLocal) | `context`(AsyncLocalStorage) |
| 中间件 | `middleware`(net/http)+ `ginmw`(gin) | `fastapi_wrap` / `flask_middleware` / `DjangoMiddleware` | `ObsFilter`(Servlet Filter) | `makeMiddleware`(express) |
| 服务端指标 | `obs_http_server_*`(中间件) | 同上 | 走 Actuator + Micrometer 官方 server instrumentation(不重复埋点) | `obs_http_server_*`(中间件) |
| 服务端指标 | `http_server_*`(中间件) | 同上 | 走 Actuator + Micrometer 官方 server instrumentation(不重复埋点) | `http_server_*`(中间件) |
| UT 数量 | 31 | 15 | 19 | 10 |
| 已发版 | ✅ `go/v1.0.0` | ❌ 版本号 `0.1.0`,未打 tag | ❌ 版本号 `0.1.0`,未打 tag | ❌ 版本号 `0.1.0`,未打 tag |
| CI | `go vet` + `go test` | `pytest`(Py3.10) | `mvn test`(JDK 17) | `npm test`(Node 20) |
Expand Down Expand Up @@ -481,7 +481,7 @@ from obs_sdk.middleware import fastapi_wrap # Flask: flask_middleware

log.init(service="<name>") # env/community 由 SDK 读 OBS_*,进程内幂等
metrics.init(service="<name>")
fastapi_wrap(app, resolver=resolve_community) # 注入 request_id + 可信判定点解析 community + 记 obs_http_server_*
fastapi_wrap(app, resolver=resolve_community) # 注入 request_id + 可信判定点解析 community + 记 http_server_*

logger = log.get_logger(__name__)
logger.info("job done", extra={"event": "release", "issue": "2061"})
Expand Down
2 changes: 1 addition & 1 deletion go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ opensourceways 微服务可观测薄封装 SDK 的 Go 实现,契约见 [spec/]
- `error` 值统一序列化为 `err.Error()` 文本(`encoding/json` 会把多数错误渲染成 `{}`)
- **指标**:`metrics` package —— `client_golang` 薄封装(counter/gauge/histogram),label 规范见 spec/metrics-format.md
- **请求上下文**:`sdkctx` —— `context.Context` 承载 `community/request_id/trace_id/span_id`
- **中间件**:`middleware`(net/http)+ `middleware/ginmw`(gin)—— 注入 request_id、可信判定点解析 community、记 `obs_http_server_*` 指标
- **中间件**:`middleware`(net/http)+ `middleware/ginmw`(gin)—— 注入 request_id、可信判定点解析 community、记 `http_server_*` 指标
- **community 双层注入**:`service/env/instance` 部署级 const label;`community` 普通可变 label —— 请求上下文覆盖,未覆盖回退部署默认(`OBS_*` 环境变量)

## 目录
Expand Down
3 changes: 2 additions & 1 deletion go/middleware/ginmw/ginmw.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Package ginmw 提供 gin 框架的观测中间件(obs-sdk-go)。
//
// 与 net/http 的 middleware 职责一致(注入 request_id / community、可选记录
// obs_http_server_* 服务器指标),只是适配 gin.HandlerFunc。gin 用户按此接入:
// http_server_* 服务器指标 —— 不加前缀,见 spec/metrics-format.md),
// 只是适配 gin.HandlerFunc。gin 用户按此接入:
//
// import "github.com/opensourceways/obs-sdk/go/middleware/ginmw"
//
Expand Down
5 changes: 3 additions & 2 deletions go/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// - 为请求注入 request_id(无则生成);有可信入站头(X-Request-Id)则沿用;
// - 可选从可信来源解析 community 写入 context(由解析函数提供,SDK 不裸透传);
// - 若配置绑定了 *metrics.Metrics,则记录 HTTP 服务器指标
// obs_http_server_requests_total / obs_http_server_request_duration_seconds。
// http_server_requests_total / http_server_request_duration_seconds(不加前缀,
// 同一条 series 已带 service label,见 spec/metrics-format.md)。
//
// 用法(net/http):
//
Expand Down Expand Up @@ -55,7 +56,7 @@ type Middleware struct {
func New(opts Options) *Middleware {
md := &Middleware{opts: opts}
if m := opts.Metrics; m != nil {
// 服务器公共指标用 obs_ 前缀(spec:不以 service 名开头,按 label 过滤)。
// 服务器公共指标不加任何前缀(spec:同一条 series 已带 service label,按 label 过滤)。
md.reqTotal = m.NewCounterVec("http_server_requests_total", "HTTP requests handled",
"method", "path", "status_code")
md.reqDuration = m.NewHistogramVec("http_server_request_duration_seconds",
Expand Down
3 changes: 2 additions & 1 deletion java/src/main/java/io/opensourceways/obssdk/ObsMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
* <li>service/env/instance 三个部署级字段注册为 <b>common tags</b>(const label);</li>
* <li><b>community 建模为普通可变 label</b>:值取请求上下文覆盖(可信判定点显式写入),
* 无覆盖时回退部署默认 —— 「注册一次两用」,单社区/多社区共用同一注册点。</li>
* <li>{@code namespace} 可选:给指标名加前缀(跨服务共享 SDK 时用)。</li>
* <li>{@code namespace} 可选:给指标名加前缀(如用 {@code service} 拼业务指标的
* {@code <service>_} 前缀);SDK 不为中间件公共指标定义前缀,别用它补前缀。</li>
* </ul>
*
* <p>注意:本 SDK 不重复造 HTTP 服务端指标 —— Java 服务通常走 Spring Boot Actuator +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ public String community() {
return community;
}

/** 可选命名空间前缀(跨服务共享 SDK 埋点时用 obs_ 等前缀区分,见 spec/metrics-format.md)。 */
/**
* 可选命名空间前缀,拼在指标名之前(如 {@code "review"} → {@code review_built_releases})。
*
* <p>默认不设 —— 业务指标按 spec 应以 {@code <service>_} 开头,接入服务可用它把
* service 前缀一并交给 SDK 拼;**不要**用来补 SDK 保留前缀,spec 不为中间件公共指标
* 定义任何前缀(同一条 series 已带 {@code service} label)。</p>
*/
public String namespace() {
return namespace;
}
Expand Down
4 changes: 2 additions & 2 deletions node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ opensourceways 微服务可观测薄封装 SDK 的 Node 实现,契约见 [spec
- **日志**:`lib/log` —— 单行 JSON 写 stream(默认 stdout)
- **指标**:`lib/metrics` —— prom-client 薄封装(counter/gauge/histogram)
- **请求上下文**:`lib/context` —— `AsyncLocalStorage` 承载 `community/request_id/trace_id/span_id`(后两者为二期 trace 预留位)
- **中间件**:`lib/middleware` —— Express/通用 HTTP 中间件(注入 request_id + 可信判定点解析 community + 记 `obs_http_server_*`)
- **中间件**:`lib/middleware` —— Express/通用 HTTP 中间件(注入 request_id + 可信判定点解析 community + 记 `http_server_*`)
- **community 双层注入**:`service/env/instance` 常驻;`community` 可变 —— 请求上下文覆盖,未覆盖回退部署默认(`OBS_*` 环境变量)

## 用法
Expand All @@ -32,7 +32,7 @@ const mw = makeMiddleware({
// community 必须在可信判定点解析(路由前缀/认证主体/白名单),见 spec/community-values.md
resolveCommunity: (req) => (req.url.startsWith('/mindspore') ? 'mindspore' : undefined),
});
// 业务 app 里 use(mw) 即可:注入 request_id + push 请求上下文 + 请求结束记 obs_http_server_* 指标
// 业务 app 里 use(mw) 即可:注入 request_id + push 请求上下文 + 请求结束记 http_server_* 指标
// /metrics 暴露:app.get('/metrics', metricsRouteHandler(m));
```

Expand Down
3 changes: 2 additions & 1 deletion node/lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//
// 职责(薄装配,见 spec/common-fields.md):注入 request_id(沿用可信入站头
// X-Request-Id 或生成);可选从可信判定点解析 community;可选记录服务器指标
// obs_http_server_requests_total / obs_http_server_request_duration_seconds。
// http_server_requests_total / http_server_request_duration_seconds(不加前缀 ——
// 同一条 series 已带 service label,见 spec/metrics-format.md)。

const { randomUUID } = require('crypto');
const context = require('./context');
Expand Down
36 changes: 29 additions & 7 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ opensourceways 微服务可观测薄封装 SDK 的 Python 实现,契约见 [sp
- **日志**:`obs_sdk.log` —— 结构化 JSON(root logger 挂唯一 JsonHandler,字段规范见 spec/log-format.md)
- **指标**:`obs_sdk.metrics` —— prometheus-client 薄封装,自带独立 CollectorRegistry
- **请求上下文**:`obs_sdk._context` —— `contextvars` 承载 `community/request_id/trace_id/span_id`(后两者为二期 trace 预留位)
- **框架适配**:`obs_sdk.middleware` —— FastAPI / Flask / Django 中间件(注入 request_id + 可信判定点解析 community)
- **框架适配**:`obs_sdk.middleware` —— FastAPI / Flask / Django 中间件(注入 request_id + 可信判定点解析 community + 记 HTTP 服务端指标
- **community 双层注入**:`service/env/instance` 常驻 const;`community` 可变 —— 请求上下文覆盖(`_context.bind`),未覆盖回退部署默认(`OBS_*` 环境变量)

## 日志用法
Expand All @@ -14,7 +14,8 @@ opensourceways 微服务可观测薄封装 SDK 的 Python 实现,契约见 [sp
import logging
from obs_sdk import log

# 字段空则回退 OBS_SERVICE / OBS_ENV / OBS_INSTANCE / OBS_COMMUNITY;进程内幂等
# 三级解析:显式参数 > OBS_SERVICE / OBS_ENV / OBS_INSTANCE / OBS_COMMUNITY > 内置默认
# 可重复调用,最后一次生效(重建 handler);请在进程启动时调用一次
log.init(service="review", env="test", instance="pod-1", community="openeuler")

logger = log.get_logger(__name__) # 命名 logger,propagate 到 root 的 JSON handler
Expand All @@ -41,7 +42,11 @@ metrics.histogram("review_duration", "评审耗时", ["api"]).observe(0.2)
# return Response(content=metrics.generate_text(), media_type=metrics.content_type())
```

`metrics.init()` 默认单例读 `OBS_*` 环境变量;多注册表场景直接 `Metrics(...)`。
`metrics.init()` 与 `log.init()` 语义一致:可重复调用、最后一次生效(重建实例与注册表);
字段同样走三级解析,`Metrics(...)` 不传参数也不会留下空 label。多注册表场景直接 `Metrics(...)`。

> 重建会换掉底层 `CollectorRegistry`:重建前注册的指标随之作废,且先前取到的 `_Vec` 句柄仍指向旧注册表。
> 所以请在进程启动时调用一次;测试里可用它重置状态。

## 请求上下文 / 中间件(community 双层注入)

Expand All @@ -55,18 +60,35 @@ from obs_sdk.middleware import fastapi_wrap, flask_middleware, DjangoMiddleware
app = fastapi_wrap(app, resolver=lambda req: "mindspore" if req.url.path.startswith("/mindspore") else None)

# Flask
flask_middleware(app, resolver=lambda: "openeuler") # 单社区可不传 resolver
flask_middleware(app, resolver=lambda req: "openeuler") # 单社区也可直接给常量

# Django(MIDDLEWARE 加 ObsMiddleware,子类里可覆写 resolve_community)
# Django(MIDDLEWARE 加 DjangoMiddleware,子类里可覆写 resolve_community)
MIDDLEWARE = [..., "obs_sdk.middleware.DjangoMiddleware"]
```

中间件会注入 `request_id`(沿用 `X-Request-Id` 或生成)并 push 请求上下文;请求内日志 / 指标自动带覆盖值,
处理结束上下文还原。

同时记 HTTP 服务端指标(spec/metrics-format.md):

| 指标 | label |
| --- | --- |
| `http_server_requests_total` | `method` `path` `status_code` |
| `http_server_request_duration_seconds` | 同上(直方图,桶边界与 Go / Node 对齐) |

`path` 取**路由模板**(`/items/{item_id}`),不是原始 URL —— 原始路径带 ID 会撑爆时序基数;
404 / 未匹配归到 `unmatched`。`service/env/instance/community` 由 SDK 自动补齐。

服务已有等价 instrumentation(如自挂 prometheus-fastapi-instrumentator)时,传
`collect_server_metrics=False` 关掉,避免同一指标被两处记录(`DjangoMiddleware` 则在子类里把
`collect_server_metrics` 置 `False`)。

## 验证

```bash
pip install -e "python[test,fastapi,flask,django]" # 或 virtualenv 装 obs_sdk + 框架
cd python && pytest
pip install -e '.[test]' # 在 python/ 下;[test] 已含 pytest + 三个 Web 框架
pytest
```

框架用 `pytest.importorskip` 跳过 —— 不装就会「静默跳过」,本地看着全绿而中间件实际零覆盖,
所以 `[test]` 里把 fastapi / flask / django 一起钉上了。
23 changes: 19 additions & 4 deletions python/obs_sdk/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
# 标识本 SDK 挂在 logger 上的 handler。
_SDK_HANDLER_NAME = "obs-sdk-json"

# SDK 自己的 logger 名。log.info(...) 这类便捷函数走它,而不是 root —— root 是
# 宿主应用的全局开关,SDK 不去改它的 level(见 get_logger 注释)。
_SDK_LOGGER_NAME = "obs_sdk"


class JsonFormatter(logging.Formatter):
"""把日志记录格式化为单行 JSON。"""
Expand Down Expand Up @@ -130,7 +134,6 @@ def init(*, service: Optional[str] = None, env: Optional[str] = None,
_log_level = getattr(logging, level.upper(), logging.INFO)

root = logging.getLogger()
root.setLevel(logging.DEBUG) # 过滤交给 formatter 层 SDK 自己的 handler 级别控制

# 移除旧 SDK handler,挂新配置的。
for h in list(root.handlers):
Expand All @@ -142,11 +145,23 @@ def init(*, service: Optional[str] = None, env: Optional[str] = None,


def get_logger(name: Optional[str] = None) -> logging.Logger:
"""返回一个 logger(命名或 root)。命名 logger 经 propagate 落到 root 的
JSON handler,单条日志只输出一次。"""
"""返回一个可直接打点的 logger。

命名 logger 经 propagate 落到 root 上 SDK 挂的 JSON handler,单条日志只输出一次。
不传名字时返回 SDK 自己的 logger(**不是 root**)。

级别只设在 SDK 交出的这个 logger 上,不碰 root:root 是宿主应用的全局开关,
把它的 level 压到 DEBUG(此前行为)会让应用自己挂在 root 上的 handler 也开始
收到 DEBUG 记录 —— 一个 SDK 不该改动宿主的全局日志级别。代价是第三方库
(uvicorn / werkzeug 等)的日志级别由应用自己的配置决定,不再被 SDK 放宽。

返回的 logger 级别由 init(level=...) 决定;应用如需另行调整,自行 setLevel 即可。
"""
if _defaults is None:
init()
return logging.getLogger(name)
logger = logging.getLogger(_SDK_LOGGER_NAME if name is None else name)
logger.setLevel(_log_level)
return logger


# --- 便捷函数(命名 = 调用方模块名) ---
Expand Down
Loading
Loading