docs/design/telemetry-resource-attributes-design.md
配套 issue: #4365 父 issue: #3731 基于 2026-05-21 对 qwen-code main 分支的代码复核
qwen-code 已经接入 OpenTelemetry SDK,但 Resource 构造方式让它在两个常见生产场景下不可用:
team / env / cost_center / user_id 标签,今天没有任何机制可以做到。即使设置标准的 OTEL_RESOURCE_ATTRIBUTES 环境变量也完全不生效。session.id 被注入到了 Resource 层,会自动附着到每条 metric 数据点。每个 CLI session 产生一个新值,指标后端(Prometheus / 阿里云 ARMS Metric / VictoriaMetrics)会被无界 time-series 撑爆。这两个问题耦合在一起:解决前者会让用户更容易给数据加高基数的字段,所以必须配套提供后者。
packages/core/src/telemetry/sdk.ts:156-161:
const resource = resourceFromAttributes({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
[SemanticResourceAttributes.SERVICE_VERSION]:
config.getCliVersion() || 'unknown',
'session.id': config.getSessionId(),
});
sdk.ts:274-278:
sdk = new NodeSDK({
resource,
// Disable async host/process/env resource detectors: they leave attributes
// pending and trigger an OTel diag.error on any resource attribute read
// before the detectors settle (e.g. during HttpInstrumentation span creation).
autoDetectResources: false,
...
});
autoDetectResources: false 关闭了标准 OTel 的 envDetector——也就是平时会读取 OTEL_RESOURCE_ATTRIBUTES 和 OTEL_SERVICE_NAME 的那一层。这是有原因的(detector 异步,会在 settle 前触发 diag.error),但副作用是这两个标准环境变量在 qwen-code 里完全无效。
session.id 实际是三重注入| 位置 | 行号 | 影响 |
|---|---|---|
| Resource | sdk.ts:160 | 所有 signal(spans / logs / metrics) |
| Per-span | session-tracing.ts:169 | spans |
| Per-log | loggers.ts:128 | logs |
getCommonAttributes() | metrics.ts:57 | 每条 metric record 显式叠加 |
也就是说单独把 session.id 从 Resource 拿掉是不够的——metrics.ts:57 的 baseMetricDefinition.getCommonAttributes() 会被 30+ 个 metric 调用点 ...spread 进去,再次塞回 session.id。
// metrics.ts:55-59
const baseMetricDefinition = {
getCommonAttributes: (config: Config): Attributes => ({
'session.id': config.getSessionId(),
}),
};
好消息:所有 metric 调用点(30+ 个)都走这一个函数,是天然的 chokepoint。
packages/core/src/telemetry/config.ts:resolveTelemetrySettings() 用统一的优先级链:
argv (highest) > QWEN_* env > OTEL_* env > settings.json (lowest)
新加项照搬这个 pattern。
packages/cli/src/config/settingsSchema.ts:998-1018 定义 telemetry 的 JSON schema:
telemetry: {
type: 'object',
// ...
jsonSchemaOverride: {
type: 'object',
properties: {
includeSensitiveSpanAttributes: { ... },
},
additionalProperties: true, // ← 今天对其他 telemetry.* key 不校验
},
}
additionalProperties: true 意味着今天 schema 对 otlpEndpoint / otlpProtocol / resourceAttributes 等其他字段全部放行不校验。新加 resourceAttributes / metrics 字段时,应同步在这里补 schema,方便 IDE 自动补全和 settings UI 渲染。
packages/core/src/telemetry/qwen-logger/qwen-logger.ts 是 qwen-code 的第一方使用上报通道(基于阿里 RUM 内部协议 RumResourceEvent),与 OTel SDK 完全独立。它有自己的 endpoint、proxy 和数据模型,不受本设计影响。详见第 3 节。
OTEL_* 环境变量| 环境变量 | 现状 |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | ✅ 支持(config.ts:79) |
OTEL_EXPORTER_OTLP_{TRACES,LOGS,METRICS}_ENDPOINT | ✅ 支持 |
OTEL_EXPORTER_OTLP_HEADERS | ✅ 底层 exporter 直接读取 |
OTEL_TRACES_SAMPLER | ✅ 支持(tracer.ts:247) |
OTEL_RESOURCE_ATTRIBUTES | ❌ 完全不支持 |
OTEL_SERVICE_NAME | ❌ 完全不支持 |
OTEL_METRICS_INCLUDE_* | ❌ 完全不支持(claude-code 风格) |
OTEL_RESOURCE_ATTRIBUTES 和自家 settings.json 给所有 OTLP 导出的 span / log / metric 附加自定义 resource attributesOTEL_SERVICE_NAME 按 OTel 规范工作(包括与 OTEL_RESOURCE_ATTRIBUTES 里的 service.name 的优先级)session.id(保护后端基数)session.id(trace correlation 必须)autoDetectResources: false,不退化 diag.error 那个已修的 bugsettingsSchema.ts 让新字段对 settings UI 和 IDE 可见qwen-logger 第一方上报:完全独立的 RUM 通道,不在本设计范围。其上报字段(device id、user agent 等)由 RUM 协议决定,不应被用户 resource attribute 干扰。若未来要给 qwen-logger 增加自定义维度,是另一条独立的设计。service.version cardinality 控制:版本变化频率有限(月级),time series 增长可控。需要时走 v2,引入 OTel View API。┌─ Resource(sdk.ts:156)────────────────────────────────────────┐
│ service.name ← OTEL_SERVICE_NAME │
│ > OTEL_RESOURCE_ATTRIBUTES.service.name│
│ > 'qwen-code' │
│ service.version ← config.getCliVersion() [reserved] │
│ ...user attrs ← OTEL_RESOURCE_ATTRIBUTES │
│ + settings.resourceAttributes │
│ ✗ session.id 移走 │
└────────────────────────────────────────────────────────────────┘
│
├──→ Spans + session.id(session-tracing.ts:169,保留)
├──→ Logs + session.id(loggers.ts:128,保留)
└──→ Metrics + getCommonAttributes() — 默认 {}
toggle ON: { session.id }
低 → 高:
OTEL_RESOURCE_ATTRIBUTES(标准 OTel env var)settings.telemetry.resourceAttributes理由:环境变量是 ops-time 临时覆盖(CI / 单机 debug),settings.json 是 fleet-baked 基线,内建是产品契约——基线优先级应高于临时变量,内建优先级应高于一切。
service.name 特殊处理service.name 必须遵守 OTel 规范:
OTEL_SERVICE_NAMEtakes precedence overservice.namedefined with theOTEL_RESOURCE_ATTRIBUTESvariable.
因此对 service.name 单独应用这条优先级链(高 → 低):
OTEL_SERVICE_NAME(最高,标准 OTel 规范规定)settings.resourceAttributes.service.name(settings 优先于 env,沿用本设计一般规则)OTEL_RESOURCE_ATTRIBUTES.service.name'qwen-code'service.name 允许通过 settings 覆盖——它是 service 身份,企业 fleet 用统一 settings.json 配置 service.name 是常见且合理的做法,禁止反而会阻断 GitOps 分发场景。OTEL_SERVICE_NAME 作为标准 OTel 规范规定的"最高优先级"通道,仍然可以在 CI / 单机调试时临时覆盖 settings。
具体规则:
| 来源 | 写入 service.name 是否生效 |
|---|---|
OTEL_SERVICE_NAME=foo | ✅ 最高优先级(覆盖任何其他来源) |
settings.resourceAttributes={ "service.name": "foo" } | ✅ 仅在没有 OTEL_SERVICE_NAME 时生效 |
OTEL_RESOURCE_ATTRIBUTES=service.name=foo | ✅ 仅在以上两者都没有时生效 |
| 键 | 用户能否覆盖 | 理由 |
|---|---|---|
service.name | ✅ env var + settings 都可(见 §4.2 优先级链) | service 身份,应允许 ops 控制 |
service.version | ❌ 任何来源都丢弃 + warn | 遥测可信度——不允许用户谎报版本 |
session.id | ❌ 任何来源都丢弃 + warn(在 metric 上额外有 toggle 控制 runtime 注入) | runtime-only;用户写到 Resource 会绕过 metric cardinality toggle(Resource attr 自动附到所有 signal) |
qwen.* 前缀 | ⚠️ 不强制保留,但 docs 建议留给产品自用 | 避免未来内建 attr 与用户 attr 冲突 |
保留键以常量集中维护:
// telemetry/resource-attributes.ts (new file)
/** Keys that cannot be overridden from any source (env or settings). */
export const RESERVED_RESOURCE_ATTRIBUTE_KEYS = new Set<string>([
'service.version',
'session.id',
]);
service.name 不在 RESERVED 列表里——它走自己的优先级链(§4.2),不属于"全局禁止覆盖"语义。RESERVED 是"任何来源写了都警告并丢弃",统一适用于 env 和 settings 两个入口。
OTEL_RESOURCE_ATTRIBUTES 解析同步实现,绕开 OTel 自带的异步 envDetector:
function parseOtelResourceAttributes(
raw: string | undefined,
): Record<string, string> {
if (!raw) return {};
const out: Record<string, string> = {};
for (const pair of raw.split(',')) {
const trimmed = pair.trim();
if (!trimmed) continue;
const idx = trimmed.indexOf('=');
if (idx <= 0) {
diag.warn(
`Skipping malformed OTEL_RESOURCE_ATTRIBUTES entry: ${trimmed}`,
);
continue;
}
const key = trimmed.slice(0, idx).trim();
const valueRaw = trimmed.slice(idx + 1).trim();
if (!key) continue;
let value: string;
try {
value = decodeURIComponent(valueRaw);
} catch {
diag.warn(
`Invalid percent-encoding in OTEL_RESOURCE_ATTRIBUTES for key "${key}", using raw value`,
);
value = valueRaw;
}
out[key] = value; // duplicate keys: last wins (matches OTel reference impls)
}
return out;
}
格式严格按 OTel 规范:key1=val1,key2=val2,值 percent-encoded。
唯一改动点 metrics.ts:55-59:
const baseMetricDefinition = {
getCommonAttributes: (config: Config): Attributes => {
const out: Attributes = {};
if (config.getTelemetryMetricsIncludeSessionId()) {
out['session.id'] = config.getSessionId();
}
return out;
},
};
调用点(30+ 个)零改动——...spread 一个空对象等价于不展开任何字段。
| 输入 | 行为 |
|---|---|
OTEL_RESOURCE_ATTRIBUTES="" (空字符串) | 返回 {},正常启动 |
OTEL_RESOURCE_ATTRIBUTES="a" (无 =) | 跳过该项 + diag.warn,继续解析其余 |
OTEL_RESOURCE_ATTRIBUTES="=val" (空 key) | 跳过该项,继续解析其余 |
OTEL_RESOURCE_ATTRIBUTES="a=,b=2" (空 value) | a='', b='2'(OTel 规范允许空 value) |
OTEL_RESOURCE_ATTRIBUTES="a=val%ZZbad" (无效 percent-encoding) | 保留原始 val%ZZbad + diag.warn |
OTEL_RESOURCE_ATTRIBUTES="a=1,a=2" (duplicate key) | 后写胜出 a=2(与 OTel SDK 参考实现一致) |
OTEL_RESOURCE_ATTRIBUTES="a=1, b=2 " (含空格) | 自动 trim |
OTEL_RESOURCE_ATTRIBUTES=service.version=x | 静默丢弃 service.version + diag.warn,保留其他键 |
settings.resourceAttributes={ "service.name": "x" } | 接受(settings 可设 service.name,见 §4.2) |
settings.resourceAttributes={ "service.version": "x" } | 静默丢弃 + diag.warn |
settings.resourceAttributes={ "team": 123 } (非 string) | TypeScript 类型阻挡;runtime 传入则 settings JSON schema validator 拒绝 |
| Resource 总大小 > OTel 限制 (4KB?) | 由底层 OTel SDK 处理,不在本层校验 |
为什么不在本层做 attribute key 命名校验(如 OTel 推荐的 [a-z][a-z0-9_.]* 模式):OTel SDK 自己会在 export 时校验,本层重复校验既慢又容易和 SDK 行为偏移。我们只做格式解析,不做语义校验。
RESERVED 键的强制保护对两个入口都生效:
// 应用于 env-parsed attrs
for (const k of RESERVED_RESOURCE_ATTRIBUTE_KEYS) {
if (k in envAttrs) {
diag.warn(`OTEL_RESOURCE_ATTRIBUTES cannot override "${k}"; ignoring`);
delete envAttrs[k];
}
}
// 应用于 settings attrs
for (const k of RESERVED_RESOURCE_ATTRIBUTE_KEYS) {
if (k in settingsAttrs) {
diag.warn(
`settings.telemetry.resourceAttributes cannot override "${k}"; ignoring`,
);
delete settingsAttrs[k];
}
}
initializeTelemetry() 时一次性构造,进程内不可变。这与 OTel SDK 设计一致。subagent-runtime.ts),共享 Resource。若未来引入跨进程 subagent,子进程会重新 init SDK,重新读 env var 和 settings——只要 env 透传过去,行为一致。refreshSessionContext() (sdk.ts:306):仅刷新 session ALS context,不重建 Resource——因为 Resource 上已经没有 session.id 了(本设计的核心改动之一)。TelemetrySettings 接口(packages/core/src/config/config.ts:293)export interface TelemetrySettings {
// ... existing fields
/** Static resource attributes attached to every span/log/metric. */
resourceAttributes?: Record<string, string>;
/** Per-signal cardinality controls. */
metrics?: {
/** Include session.id on metric data points (default: false). */
includeSessionId?: boolean;
};
}
Config getter(同文件)class Config {
getTelemetryResourceAttributes(): Record<string, string> {
return this.telemetrySettings.resourceAttributes ?? {};
}
getTelemetryMetricsIncludeSessionId(): boolean {
return this.telemetrySettings.metrics?.includeSessionId ?? false;
}
}
resolveTelemetrySettings() 新增const envResourceAttrs = parseOtelResourceAttributes(
env['OTEL_RESOURCE_ATTRIBUTES'],
);
const settingsResourceAttrs = { ...(settings.resourceAttributes ?? {}) };
// Strip RESERVED keys from both sources (warn if user tried to set them).
for (const k of RESERVED_RESOURCE_ATTRIBUTE_KEYS) {
if (k in envResourceAttrs) {
diag.warn(`OTEL_RESOURCE_ATTRIBUTES cannot override "${k}"; ignoring`);
delete envResourceAttrs[k];
}
if (k in settingsResourceAttrs) {
diag.warn(
`settings.telemetry.resourceAttributes cannot override "${k}"; ignoring`,
);
delete settingsResourceAttrs[k];
}
}
// Merge: env < settings (settings wins on conflict).
const merged: Record<string, string> = {
...envResourceAttrs,
...settingsResourceAttrs,
};
// service.name precedence: OTEL_SERVICE_NAME (env-only escape) wins over
// everything else. settings already overwrote env in the spread above.
if (env['OTEL_SERVICE_NAME']) {
merged['service.name'] = env['OTEL_SERVICE_NAME'];
}
const resourceAttributes = merged;
const metricsIncludeSessionId =
parseBooleanEnvFlag(env['QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID']) ??
settings.metrics?.includeSessionId ??
false;
return {
// ... existing fields
resourceAttributes,
metrics: { includeSessionId: metricsIncludeSessionId },
};
sdk.ts Resource 构造改动const userAttrs = config.getTelemetryResourceAttributes();
// service.version is always built-in; service.name flows through userAttrs
// (it was already resolved with OTEL_SERVICE_NAME precedence in resolver).
const builtinServiceName = userAttrs['service.name'] ?? SERVICE_NAME;
const { 'service.name': _, 'service.version': __, ...nonReserved } = userAttrs;
const resource = resourceFromAttributes({
...nonReserved,
[SemanticResourceAttributes.SERVICE_NAME]: builtinServiceName,
[SemanticResourceAttributes.SERVICE_VERSION]:
config.getCliVersion() || 'unknown',
// session.id deliberately NOT placed on Resource — see design doc §4.1
});
settingsSchema.ts 改动packages/cli/src/config/settingsSchema.ts:998-1018 的 telemetry.jsonSchemaOverride.properties 加:
{
// ... existing includeSensitiveSpanAttributes
resourceAttributes: {
type: 'object',
additionalProperties: { type: 'string' },
description:
'Static resource attributes attached to all telemetry data. ' +
'Keys must be strings; values must be strings. ' +
'Reserved keys (service.name, service.version) are silently dropped.',
default: {},
},
metrics: {
type: 'object',
additionalProperties: false,
properties: {
includeSessionId: {
type: 'boolean',
default: false,
description:
'Include session.id on every metric data point. ' +
'WARNING: each CLI session creates a new value, causing unbounded ' +
'metric time-series fan-out. Only enable for short-term debugging.',
},
},
},
}
也要把 additionalProperties: true 重新评估——目前是 permissive,可以保留也可以转 strict。建议保留 permissive,避免对其他未在 schema 中声明的 telemetry.* 字段产生破坏性变更,但 docs 里明确"未声明字段会被忽略"。
| 文件 | 改动 |
|---|---|
packages/core/src/telemetry/sdk.ts | 改 Resource 构造(合并 user attrs,删 session.id) |
packages/core/src/telemetry/resource-attributes.ts (新文件) | parseOtelResourceAttributes() + RESERVED_RESOURCE_ATTRIBUTE_KEYS 常量 |
packages/core/src/telemetry/config.ts | resolver 加 resourceAttributes + metrics.includeSessionId 解析与 merge |
packages/core/src/telemetry/metrics.ts | getCommonAttributes() 加 toggle gate |
packages/core/src/config/config.ts | TelemetrySettings schema + 两个 getter |
packages/cli/src/config/settingsSchema.ts | jsonSchemaOverride 加 resourceAttributes + metrics |
docs/developers/development/telemetry.md | 加 "Resource attributes" + "Cardinality controls" 两节 + 迁移说明 + 示例 |
packages/core/src/telemetry/resource-attributes.test.ts (新) | 解析器单元测试(覆盖 §4.6 全部用例) |
packages/core/src/telemetry/sdk.test.ts | merge 优先级 / 保留键 / OTEL_SERVICE_NAME |
packages/core/src/telemetry/metrics.test.ts | toggle off/on 时 session.id 出现与否 |
packages/core/src/telemetry/config.test.ts | env / settings 合并 |
CHANGELOG.md 或 release notes | PR 2 的 breaking change 说明 |
按 review 友好性与 blast radius 分三个 PR:
resource-attributes.ts:parseOtelResourceAttributes() + RESERVED_RESOURCE_ATTRIBUTE_KEYSTelemetrySettings.resourceAttributes 字段 + resolver merge 逻辑OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES 接入,按 §4.2 优先级sdk.ts)settingsSchema.ts 加 resourceAttributes JSON schemasession.id 在 Resource 上的位置风险:低。完全 additive,不改任何现有行为。除非用户主动设置环境变量或 settings,否则导出的数据无变化。
session.id (sdk.ts:160 那一行)metrics.includeSessionId toggle(settings + env)+ getCommonAttributes() gatesettingsSchema.ts 加 metrics JSON schema风险:中等。任何依赖 metric 上 session.id 的 Prometheus query / Grafana dashboard / 告警规则会失效。需要显式 release note 与 1-2 个版本的迁移窗口。
Opt-in 过渡方案(候选,本期建议不采用):
PR 2 可先以"opt-out"形式落地——默认仍把
session.id注入 metric,但加 warn log "this default will flip in v0.X"。一个 release 后再翻转默认。
不建议采用的原因:(1)当前 qwen-code 用户群不大,破坏面有限;(2)这是 cardinality bug,越早默认安全越好;(3)双段式发布会增加文档负担。如果父 issue owner 想要保守一些,可以采纳。
docs/developers/development/telemetry.md 补示例(见 §10)parseOtelResourceAttributes() 单元测试参数化覆盖 §4.6 表格全部行(建议用 vitest it.each):
it.each([
['', {}],
['a=1', { a: '1' }],
['a=1,b=2', { a: '1', b: '2' }],
['a=hello%20world', { a: 'hello world' }],
['a=val%ZZbad', { a: 'val%ZZbad' }], // invalid percent
['malformed', {}],
['=val', {}],
['a=', { a: '' }],
['a=1,a=2', { a: '2' }],
[' a = 1 , b = 2 ', { a: '1', b: '2' }],
])('parses %j → %j', (input, expected) => {
expect(parseOtelResourceAttributes(input)).toEqual(expected);
});
| 场景 | 期望 service.name | 期望 user attr |
|---|---|---|
| 全空 | 'qwen-code' | 不存在 |
仅 env OTEL_SERVICE_NAME=A | 'A' | — |
仅 env OTEL_RESOURCE_ATTRIBUTES=service.name=B | 'B' | — |
OTEL_SERVICE_NAME=A + OTEL_RESOURCE_ATTRIBUTES=service.name=B | 'A'(OTEL_SERVICE_NAME 优先) | — |
OTEL_SERVICE_NAME=A + settings={service.name:C} | 'A'(OTEL_SERVICE_NAME 优先) | — |
OTEL_RESOURCE_ATTRIBUTES=service.name=B + settings={service.name:C} | 'C'(settings 优先于 env,无 OTEL_SERVICE_NAME 时) | — |
OTEL_RESOURCE_ATTRIBUTES=team=x + settings={team:y} | 'qwen-code' | team='y'(settings 优先) |
OTEL_RESOURCE_ATTRIBUTES=service.version=fake | 'qwen-code' + warn | service.version 仍为真实 cli version |
settings={service.version:fake} | 'qwen-code' + warn | service.version 仍为真实 cli version |
用 InMemorySpanExporter 拿一个 span,断言:
expect(span.resource.attributes['service.name']).toBe('qwen-code');
expect(span.resource.attributes['service.version']).toBe(EXPECTED_VERSION);
expect(span.resource.attributes['session.id']).toBeUndefined(); // 关键
expect(span.resource.attributes['team']).toBe('platform'); // 用户加的
it('does not emit session.id on metrics by default', async () => {
// emit one tool call counter
recordToolCallMetrics(...);
const data = await metricReader.collect();
const dp = data.resourceMetrics.scopeMetrics[0].metrics[0].dataPoints[0];
expect(dp.attributes['session.id']).toBeUndefined();
});
it('emits session.id when toggle is true', async () => {
config.telemetrySettings.metrics = { includeSessionId: true };
recordToolCallMetrics(...);
const data = await metricReader.collect();
const dp = data.resourceMetrics.scopeMetrics[0].metrics[0].dataPoints[0];
expect(dp.attributes['session.id']).toBe(KNOWN_SESSION_ID);
});
session.id(不受 metric toggle 影响)session.id(不受 metric toggle 影响)autoDetectResources: false 保持不变(assertion on config)diag.error(捕获 OTel diag 日志做 assertion)校验下列输入都触发 diag.warn 一次:
settings.resourceAttributes = { 'service.version': 'x' }(reserved)OTEL_RESOURCE_ATTRIBUTES=service.version=x(reserved,env 也要 warn)OTEL_RESOURCE_ATTRIBUTES=malformed(无 =)OTEL_RESOURCE_ATTRIBUTES=a=val%ZZ(无效 percent-encoding)校验下列输入不触发 warn(合法路径):
settings.resourceAttributes = { 'service.name': 'x' }(settings 允许设 service.name)OTEL_SERVICE_NAME=foo + settings.resourceAttributes = { 'service.name': 'bar' }(OTEL_SERVICE_NAME 优先即可,不需要 warn)指标上的 session.id 默认消失。这会影响:
by (session_id) / group_left(session_id) 的聚合注:spans 和 logs 上的 session.id 不受影响。
文档里给两个选项:
选项 A:恢复旧行为(短期 debug 推荐)
export QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID=true
或 settings.json:
{
"telemetry": {
"metrics": { "includeSessionId": true }
}
}
⚠️ 警告:长期开启会让 metric time-series 数量 = 历史 session 数量,撑爆后端。仅短期 debug 用。
选项 B:改用 spans / logs 做 session 切片(推荐)
session.id,可在 trace backend(如 Jaeger / Aliyun ARMS Tracing)/ log backend(如 Loki / SLS)按 session 切片**Breaking change (metric attribute):**
The `session.id` attribute is no longer attached to metric data
points by default. This protects metric backends from unbounded
time-series fan-out.
- Spans and logs are unaffected — `session.id` is still present.
- To restore the previous behavior (short-term debugging only), set
`QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID=true` or in settings.json:
`telemetry.metrics.includeSessionId: true`.
- For long-term session correlation, query against trace / log
backends instead of metric backends.
See docs/developers/development/telemetry.md "Migration" for details.
export OTEL_RESOURCE_ATTRIBUTES="team=platform,env=prod,cost_center=eng-123"
效果:所有 span / log / metric 都带 team=platform env=prod cost_center=eng-123。
OTEL_SERVICE_NAME 在共享 collector 中路由export OTEL_SERVICE_NAME=qwen-code-ci
效果:service.name=qwen-code-ci,多租户 OTel collector 可按 service.name 路由到不同后端。
公司 fleet 的 ~/.qwen/settings.json(GitOps 分发):
{
"telemetry": {
"resourceAttributes": {
"deployment.environment": "production",
"service.namespace": "engineering-tooling"
}
}
}
单机 ops 临时覆盖(不修改 settings):
export OTEL_RESOURCE_ATTRIBUTES="debug_run=true"
# settings 里的 deployment.environment / service.namespace 仍然生效
# 同时这次运行额外带 debug_run=true
# 一次性 debug run
QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID=true qwen "投资分析"
完事即关闭,不要持久化到 settings。
{
"telemetry": {
"enabled": true,
"otlpEndpoint": "http://<arms-endpoint>/api/v1/...",
"otlpProtocol": "http",
"resourceAttributes": {
"team": "platform",
"deployment.environment": "production"
},
"metrics": {
"includeSessionId": false
}
}
}
| 维度 | claude-code | qwen-code 本设计 | 决策依据 |
|---|---|---|---|
| 标准 OTel env var | OTEL_RESOURCE_ATTRIBUTES / OTEL_SERVICE_NAME | ✅ 一致 | 标准契约 |
OTEL_SERVICE_NAME 优先级 | 遵守 OTel 规范 | ✅ 遵守 | spec 明确规定 |
| Cardinality 开关命名 | OTEL_METRICS_INCLUDE_* | QWEN_TELEMETRY_METRICS_INCLUDE_* | 不污染标准 OTel 命名空间 |
| 开关作用域 | 仅 metric | ✅ 仅 metric | spans / logs 是 per-event,无 cardinality 爆炸问题 |
| 默认值 | 高基数 attribute 默认 false | ✅ 默认 false | 安全优先 |
| Per-attribute granularity | 每 attribute 一个 toggle | ✅ 一致 | 灵活,符合实际诊断需求 |
| settings.json 等价物 | ❌ 无 | ✅ 有 telemetry.resourceAttributes + metrics | 企业 fleet 部署 base config |
| Per-span 动态 hook | ❌ 无 | ❌ 无 | 复杂度高,claude-code 也没解,本期不做 |
多租户 account_uuid | 有 | ❌ 无 | qwen-code metric 里没有此 attr |
Agent SDK options.env | 有 | ❌ 无 | qwen-code 没有等价模式 |
| 保留键策略 | 不允许覆盖 built-in id | ✅ 一致 | 遥测可信度 |
| 第一方上报通道 | claude-code 也有独立第一方通道(与 OTel 隔离) | ✅ qwen-logger 同样隔离 | 第一方与第三方通道职责分离 |
最值得借的两点:
*_INCLUDE_* 一眼能看出语义,比反义命名(*_EXCLUDE_* / *_DROP_*)清晰qwen-code 做得更好的点:
service.version 不可覆盖):减少遥测被污染的可能service.version cardinality 控制:用 OTel View API 在 metric 层 drop attributeuser.account_uuid / model 等,按需补 toggleOnSpanStart(span, context) => attrs 回调。需要独立设计。service.* 前缀以外的内建 attr),目前靠保留键列表硬编码够用。