ai-otel-setup 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +181 -11
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const fs = require("fs");
|
|
25
25
|
const path = require("path");
|
|
26
26
|
const os = require("os");
|
|
27
|
+
const { execFileSync } = require("child_process");
|
|
27
28
|
|
|
28
29
|
const PKG_VERSION = require("./package.json").version;
|
|
29
30
|
|
|
@@ -131,6 +132,164 @@ function mergeNoProxy(existing, host) {
|
|
|
131
132
|
return list.join(",");
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// ---------- git config 兜底 (跨平台) ----------
|
|
136
|
+
//
|
|
137
|
+
// hook 进程偶有"压根没跑"的场景(网络/超时/进程崩溃),导致 git.user.email/name 永久丢失。
|
|
138
|
+
// 装机时把全局 git config 写到 OTEL_RESOURCE_ATTRIBUTES,CC SDK 自动把 resource attr
|
|
139
|
+
// 带到每条 metric/log,service 端在 SessionStore miss 时用它兜底(参见 translator.js
|
|
140
|
+
// 的 RESOURCE_FALLBACK_KEYS)。
|
|
141
|
+
//
|
|
142
|
+
// 跨平台细节:
|
|
143
|
+
// - execFileSync(cmd, args):不经过 shell,Win/Mac 行为一致
|
|
144
|
+
// - windowsHide:true:Windows 上不弹 cmd 黑窗
|
|
145
|
+
// - stdio[2]="ignore":屏蔽 stderr,避免 git 报错刷屏
|
|
146
|
+
// - timeout:1000:超时直接当成"读不到",不让 installer 卡住
|
|
147
|
+
// - ENOENT (git 没装) / 退出码非 0 (key 没设) 都吞掉返回空串
|
|
148
|
+
function readGlobalGitUser() {
|
|
149
|
+
function readGitVal(key) {
|
|
150
|
+
try {
|
|
151
|
+
return execFileSync("git", ["config", "--global", "--get", key], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
timeout: 1000,
|
|
155
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
156
|
+
}).trim();
|
|
157
|
+
} catch (_) {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
name: readGitVal("user.name"),
|
|
163
|
+
email: readGitVal("user.email"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------- 装机上报到 cc-view-server ----------
|
|
168
|
+
//
|
|
169
|
+
// 安装完成时打一发 POST 到 cc-view-server,让运营侧能看到"谁/在哪台机/装了哪个版本"。
|
|
170
|
+
// 设计原则:
|
|
171
|
+
// - fire-and-forget:3s 超时、不重试、任何失败绝不让安装本身退出非 0
|
|
172
|
+
// - 复用用户传给 OTel collector 的 host:172.31.250.57,port 8081 写死
|
|
173
|
+
// - URL 本身是公司内网地址,自带隐式凭据,不带 SSO header
|
|
174
|
+
// - debug 模式下才打错误,正常运行不污染 stdout
|
|
175
|
+
|
|
176
|
+
function buildReportUrl(otelEndpoint) {
|
|
177
|
+
try {
|
|
178
|
+
const u = new URL(otelEndpoint);
|
|
179
|
+
// cc-view-server 跑在同机 :8081(与 collector 4317/4318 同主机)
|
|
180
|
+
return `http://${u.hostname}:8081/api/installer/report`;
|
|
181
|
+
} catch (_) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function postJsonWithTimeout(targetUrl, payload, timeoutMs) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
let u;
|
|
189
|
+
try {
|
|
190
|
+
u = new URL(targetUrl);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return reject(e);
|
|
193
|
+
}
|
|
194
|
+
const isHttps = u.protocol === "https:";
|
|
195
|
+
const lib = isHttps ? require("https") : require("http");
|
|
196
|
+
const body = Buffer.from(JSON.stringify(payload), "utf8");
|
|
197
|
+
const req = lib.request(
|
|
198
|
+
{
|
|
199
|
+
method: "POST",
|
|
200
|
+
hostname: u.hostname,
|
|
201
|
+
port: u.port || (isHttps ? 443 : 80),
|
|
202
|
+
path: (u.pathname || "/") + (u.search || ""),
|
|
203
|
+
headers: {
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
"Content-Length": body.length,
|
|
206
|
+
},
|
|
207
|
+
timeout: timeoutMs,
|
|
208
|
+
},
|
|
209
|
+
(res) => {
|
|
210
|
+
// 排空 body,让 socket 进入 keepalive/释放
|
|
211
|
+
res.on("data", () => {});
|
|
212
|
+
res.on("end", () => resolve(res.statusCode || 0));
|
|
213
|
+
res.on("error", reject);
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
req.on("error", reject);
|
|
217
|
+
req.on("timeout", () => {
|
|
218
|
+
req.destroy(new Error("timeout"));
|
|
219
|
+
});
|
|
220
|
+
req.write(body);
|
|
221
|
+
req.end();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function reportInstall(otelEndpoint, gitUser, allResults, debug) {
|
|
226
|
+
if (!gitUser || !gitUser.email) {
|
|
227
|
+
if (debug) console.error("[ai-otel-setup] 跳过装机上报:无 git user.email");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const reportUrl = buildReportUrl(otelEndpoint);
|
|
231
|
+
if (!reportUrl) return;
|
|
232
|
+
const findOk = (tool) =>
|
|
233
|
+
allResults.find((r) => r.tool === tool)?.status === "installed";
|
|
234
|
+
const payload = {
|
|
235
|
+
git_email: gitUser.email,
|
|
236
|
+
git_name: gitUser.name || "",
|
|
237
|
+
hostname: os.hostname(),
|
|
238
|
+
installer_version: PKG_VERSION,
|
|
239
|
+
os_platform: os.platform(),
|
|
240
|
+
os_arch: os.arch(),
|
|
241
|
+
node_version: process.version,
|
|
242
|
+
cc_cli_detected: findOk("claude") ? 1 : 0,
|
|
243
|
+
codex_cli_detected: findOk("codex") ? 1 : 0,
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
await postJsonWithTimeout(reportUrl, payload, 3000);
|
|
247
|
+
if (debug) console.error("[ai-otel-setup] 装机上报已发送");
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if (debug) {
|
|
250
|
+
console.error("[ai-otel-setup] 装机上报失败(不影响安装):", e.message || e);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------- OTEL_RESOURCE_ATTRIBUTES (W3C baggage 风格) ----------
|
|
256
|
+
|
|
257
|
+
// "k1=urlencoded,k2=urlencoded2" → { k1: "decoded", k2: "decoded2" }
|
|
258
|
+
function parseResourceAttrs(s) {
|
|
259
|
+
const out = {};
|
|
260
|
+
if (!s || typeof s !== "string") return out;
|
|
261
|
+
for (const pair of s.split(",")) {
|
|
262
|
+
const idx = pair.indexOf("=");
|
|
263
|
+
if (idx <= 0) continue;
|
|
264
|
+
const k = pair.slice(0, idx).trim();
|
|
265
|
+
if (!k) continue;
|
|
266
|
+
const raw = pair.slice(idx + 1).trim();
|
|
267
|
+
try {
|
|
268
|
+
out[k] = decodeURIComponent(raw);
|
|
269
|
+
} catch (_) {
|
|
270
|
+
out[k] = raw; // decode 失败原样保留,不抛
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function serializeResourceAttrs(obj) {
|
|
277
|
+
const parts = [];
|
|
278
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
279
|
+
if (v === "" || v === null || v === undefined) continue;
|
|
280
|
+
parts.push(`${k}=${encodeURIComponent(v)}`);
|
|
281
|
+
}
|
|
282
|
+
return parts.join(",");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// parse-merge-serialize:保留用户自定义 attr(如 region=us-east),仅注入/覆盖 git.user.*
|
|
286
|
+
function mergeResourceAttrs(existing, gitUser) {
|
|
287
|
+
const attrs = parseResourceAttrs(existing || "");
|
|
288
|
+
if (gitUser.email) attrs["git.user.email"] = gitUser.email;
|
|
289
|
+
if (gitUser.name) attrs["git.user.name"] = gitUser.name;
|
|
290
|
+
return serializeResourceAttrs(attrs);
|
|
291
|
+
}
|
|
292
|
+
|
|
134
293
|
// ---------- 文件操作 ----------
|
|
135
294
|
|
|
136
295
|
function readJSONSafe(p) {
|
|
@@ -164,12 +323,11 @@ function backup(p) {
|
|
|
164
323
|
function buildEnv(template, args, endpoint) {
|
|
165
324
|
const env = { ...template.env };
|
|
166
325
|
env.OTEL_EXPORTER_OTLP_ENDPOINT = endpoint;
|
|
167
|
-
// OTEL_RESOURCE_ATTRIBUTES
|
|
168
|
-
delete env.OTEL_RESOURCE_ATTRIBUTES;
|
|
326
|
+
// OTEL_RESOURCE_ATTRIBUTES 由 mergeSettings 单独处理(parse-merge 用户已有 + 注入 git.user.*)
|
|
169
327
|
return env;
|
|
170
328
|
}
|
|
171
329
|
|
|
172
|
-
function mergeSettings(existing, newEnv, hookEntry, promptHookEntry, collectorHost) {
|
|
330
|
+
function mergeSettings(existing, newEnv, hookEntry, promptHookEntry, collectorHost, gitUser) {
|
|
173
331
|
const merged = { ...existing };
|
|
174
332
|
|
|
175
333
|
// env:plugin 优先(组织规范不允许个人改红线),但保留用户独有的 env
|
|
@@ -177,8 +335,14 @@ function mergeSettings(existing, newEnv, hookEntry, promptHookEntry, collectorHo
|
|
|
177
335
|
for (const k of OTEL_KEYS) {
|
|
178
336
|
merged.env[k] = newEnv[k];
|
|
179
337
|
}
|
|
180
|
-
|
|
181
|
-
|
|
338
|
+
|
|
339
|
+
// OTEL_RESOURCE_ATTRIBUTES:parse-merge 用户已有 attr + 注入 git.user.email/name。
|
|
340
|
+
// 不进 OTEL_KEYS(OTEL_KEYS 走 overwrite,会丢掉用户自定义如 region=us-east)。
|
|
341
|
+
// 只在 readGlobalGitUser 拿到非空值时写;全空时保持用户已有值不动(包括不删)。
|
|
342
|
+
if (gitUser && (gitUser.name || gitUser.email)) {
|
|
343
|
+
const ra = mergeResourceAttrs(merged.env.OTEL_RESOURCE_ATTRIBUTES, gitUser);
|
|
344
|
+
if (ra) merged.env.OTEL_RESOURCE_ATTRIBUTES = ra;
|
|
345
|
+
}
|
|
182
346
|
|
|
183
347
|
// 兜底用户写坏的 HTTP(S)_PROXY:把 collector host 加进 NO_PROXY,让 OTel gRPC 绕过代理
|
|
184
348
|
// 仅追加,不动用户原有的 NO_PROXY 值,也不动 HTTP_PROXY / HTTPS_PROXY
|
|
@@ -424,7 +588,7 @@ function installGemini(home, endpoint) {
|
|
|
424
588
|
|
|
425
589
|
// ---------- 主流程 ----------
|
|
426
590
|
|
|
427
|
-
function main() {
|
|
591
|
+
async function main() {
|
|
428
592
|
const args = parseArgs(process.argv.slice(2));
|
|
429
593
|
|
|
430
594
|
if (args.help || args.h || process.argv.includes("--help")) {
|
|
@@ -505,6 +669,10 @@ function main() {
|
|
|
505
669
|
logsEndpoint: logsEndpointFromGrpc(endpoint),
|
|
506
670
|
});
|
|
507
671
|
|
|
672
|
+
// 读全局 git config,作为 hook 进程没跑时的 SDK 层兜底来源
|
|
673
|
+
// 失败/缺失返回空串;mergeSettings 见空就跳过 OTEL_RESOURCE_ATTRIBUTES 写入
|
|
674
|
+
const gitUser = readGlobalGitUser();
|
|
675
|
+
|
|
508
676
|
const existing = readJSONSafe(settingsPath);
|
|
509
677
|
const bak = backup(settingsPath);
|
|
510
678
|
const merged = mergeSettings(
|
|
@@ -512,7 +680,8 @@ function main() {
|
|
|
512
680
|
newEnv,
|
|
513
681
|
hookEntry,
|
|
514
682
|
promptHookEntry,
|
|
515
|
-
extractHost(endpoint)
|
|
683
|
+
extractHost(endpoint),
|
|
684
|
+
gitUser
|
|
516
685
|
);
|
|
517
686
|
writeJSONAtomic(settingsPath, merged);
|
|
518
687
|
|
|
@@ -555,6 +724,9 @@ function main() {
|
|
|
555
724
|
"SessionStart 中 id=" + HOOK_ID + " 与 UserPromptSubmit 中 id=" + PROMPT_HOOK_ID + " 的条目。"
|
|
556
725
|
);
|
|
557
726
|
}
|
|
727
|
+
|
|
728
|
+
// 装机上报:fire-and-forget 语义,3s 内完成或放弃;任何错误都不冒泡
|
|
729
|
+
await reportInstall(endpoint, gitUser, allResults, debug);
|
|
558
730
|
}
|
|
559
731
|
|
|
560
732
|
function printUsage() {
|
|
@@ -569,9 +741,7 @@ function printUsage() {
|
|
|
569
741
|
`);
|
|
570
742
|
}
|
|
571
743
|
|
|
572
|
-
|
|
573
|
-
main();
|
|
574
|
-
} catch (e) {
|
|
744
|
+
main().catch((e) => {
|
|
575
745
|
console.error("[ai-otel-setup] 失败:" + (e && e.message ? e.message : e));
|
|
576
746
|
process.exit(1);
|
|
577
|
-
}
|
|
747
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-otel-setup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "One-shot installer for AI CLI OpenTelemetry forwarding. Writes Claude Code, Codex CLI, and Gemini CLI telemetry config in a single npx command.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-otel-setup": "cli.js",
|