@lark-apaas/miaoda-cli 0.1.2-alpha.b2b5ae5 → 0.1.2-alpha.b6d5335
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/dist/api/deploy/schemas.js +1 -1
- package/dist/cli/commands/deploy/index.js +9 -8
- package/dist/cli/commands/observability/index.js +7 -5
- package/dist/cli/commands/shared.js +24 -0
- package/dist/cli/handlers/observability/analytics.js +3 -2
- package/dist/cli/handlers/observability/metric.js +1 -1
- package/dist/utils/http.js +88 -2
- package/package.json +1 -1
|
@@ -97,7 +97,7 @@ exports.errorJobSchema = {
|
|
|
97
97
|
columns: [
|
|
98
98
|
{ key: "jobID", label: "job-id" },
|
|
99
99
|
{ key: "componentName", label: "component" },
|
|
100
|
-
{ key: "errorMsg", label: "error"
|
|
100
|
+
{ key: "errorMsg", label: "error" },
|
|
101
101
|
],
|
|
102
102
|
strict: true,
|
|
103
103
|
};
|
|
@@ -20,6 +20,12 @@ function registerDeployCommands(program) {
|
|
|
20
20
|
.option("--wait", "阻塞直到流水线终态", false)
|
|
21
21
|
.option("--timeout <sec>", "--wait 最长等待秒数(默认 300)", parseTimeout, 300)
|
|
22
22
|
.addHelpText("after", `
|
|
23
|
+
部署前置检查(Agent 必须执行)
|
|
24
|
+
miaoda deploy 跑的是当前分支的远端 HEAD——本地未 commit / 未 push 的代码不会进发布产物。
|
|
25
|
+
调用前先 \`git status\` + \`git rev-list --count @{u}..HEAD\` 确认;有未提交 / 未推送时:
|
|
26
|
+
- 是本次任务的产物 → 先 commit + push 再 deploy
|
|
27
|
+
- 不能确定意图 → 用 AskUser 跟用户确认(不要默默继续)
|
|
28
|
+
|
|
23
29
|
JSON 输出
|
|
24
30
|
不带 --wait:{"data": {"pipelineTaskID": "..."}}(pipelineTaskID 即 deploy-id)
|
|
25
31
|
带 --wait:data 额外包含 detail(pipeline 终态)
|
|
@@ -70,14 +76,9 @@ function registerDeployHistory(parent) {
|
|
|
70
76
|
.command("history")
|
|
71
77
|
.description("查询发布历史(按时间倒序,分页)")
|
|
72
78
|
.addOption((0, shared_1.appIdOption)().hideHelp())
|
|
73
|
-
.addOption(new commander_1.Option("--status <status>", "状态过滤(pipeline
|
|
74
|
-
"todo",
|
|
75
|
-
"running",
|
|
76
|
-
"success",
|
|
77
|
-
"failed",
|
|
78
|
-
"canceled",
|
|
79
|
-
"hold_on",
|
|
80
|
-
]))
|
|
79
|
+
.addOption(new commander_1.Option("--status <status>", "状态过滤(pipeline 节点状态,大小写不敏感)")
|
|
80
|
+
.choices(["todo", "running", "success", "failed", "canceled", "hold_on"])
|
|
81
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["todo", "running", "success", "failed", "canceled", "hold_on"])))
|
|
81
82
|
.option("--since <time>", "起始时间")
|
|
82
83
|
.option("--until <time>", "截止时间")
|
|
83
84
|
.option("--limit <n>", "返回条数上限(1~100)", parseLimit, 50)
|
|
@@ -23,8 +23,7 @@ function registerObservabilityCommands(program) {
|
|
|
23
23
|
应用上下文:--app-id <id> 或环境变量 MIAODA_APP_ID。
|
|
24
24
|
|
|
25
25
|
应用环境
|
|
26
|
-
|
|
27
|
-
metric/analytics 不需要 --env。
|
|
26
|
+
目前只支持线上环境。
|
|
28
27
|
`);
|
|
29
28
|
registerLog(obCmd);
|
|
30
29
|
registerTrace(obCmd);
|
|
@@ -37,7 +36,9 @@ function registerLog(parent) {
|
|
|
37
36
|
.command("log")
|
|
38
37
|
.description("查询线上运行日志(按时间倒序,分页)")
|
|
39
38
|
.addOption((0, shared_1.appIdOption)().hideHelp())
|
|
40
|
-
.addOption(new commander_1.Option("--level <level>", "
|
|
39
|
+
.addOption(new commander_1.Option("--level <level>", "日志级别(大小写不敏感)")
|
|
40
|
+
.choices(["DEBUG", "INFO", "WARN", "ERROR"])
|
|
41
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["DEBUG", "INFO", "WARN", "ERROR"])))
|
|
41
42
|
.option("--since <time>", "开始时间")
|
|
42
43
|
.option("--until <time>", "截止时间")
|
|
43
44
|
.option("--trace-id <id>", "按 trace ID 过滤")
|
|
@@ -156,9 +157,10 @@ function registerMetric(parent) {
|
|
|
156
157
|
.option("--series <name>", "过滤图表中的某条线:latency 取 p50/p99;requests 取 total/error;缺省返回所有相关线")
|
|
157
158
|
.option("--since <time>", "开始时间")
|
|
158
159
|
.option("--until <time>", "截止时间")
|
|
159
|
-
.addOption(new commander_1.Option("--down-sample <duration>", "降采样粒度(1m=1分钟 / 1h=1小时 / 1d=1
|
|
160
|
+
.addOption(new commander_1.Option("--down-sample <duration>", "降采样粒度(1m=1分钟 / 1h=1小时 / 1d=1天,大小写不敏感)")
|
|
160
161
|
.choices(["1m", "1h", "1d"])
|
|
161
|
-
.
|
|
162
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["1m", "1h", "1d"]))
|
|
163
|
+
.default("1m"))
|
|
162
164
|
.addHelpText("after", `${COMMON_TIME_HELP}
|
|
163
165
|
JSON 输出
|
|
164
166
|
{"data": [{"metricName": "...", "dimensions": {...}, "dataPoints": [...]}], "next_cursor": null, "has_more": false}
|
|
@@ -6,6 +6,7 @@ exports.softRequiredOption = softRequiredOption;
|
|
|
6
6
|
exports.resolveAppId = resolveAppId;
|
|
7
7
|
exports.withHelp = withHelp;
|
|
8
8
|
exports.failArgs = failArgs;
|
|
9
|
+
exports.caseInsensitiveChoice = caseInsensitiveChoice;
|
|
9
10
|
exports.rejectCliOverride = rejectCliOverride;
|
|
10
11
|
const commander_1 = require("commander");
|
|
11
12
|
const error_1 = require("../../utils/error");
|
|
@@ -62,6 +63,29 @@ function withHelp(cmd, handler) {
|
|
|
62
63
|
function failArgs(message) {
|
|
63
64
|
throw new error_1.AppError("ARGS_INVALID", message);
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* 大小写不敏感的 choice argParser:把用户输入按 lowerCase 匹配回 canonical 数组里
|
|
68
|
+
* 的同名值(保留 canonical 大小写),未命中抛 Commander 的 InvalidArgumentError
|
|
69
|
+
* (exit code 1,自带友好错误)。
|
|
70
|
+
*
|
|
71
|
+
* 必须先 .choices(canonical)、再 .argParser()——Commander 内部 .choices() 会
|
|
72
|
+
* 重置 parseArg,反过来调会让 argParser 失效。.choices() 仅留给 help 渲染
|
|
73
|
+
* "choices: A, B, C",实际白名单校验由 argParser 接管。
|
|
74
|
+
*
|
|
75
|
+
* new Option("--level <level>", "日志级别(不区分大小写)")
|
|
76
|
+
* .choices(["DEBUG", "INFO", "WARN", "ERROR"])
|
|
77
|
+
* .argParser(caseInsensitiveChoice(["DEBUG", "INFO", "WARN", "ERROR"]));
|
|
78
|
+
*/
|
|
79
|
+
function caseInsensitiveChoice(canonical) {
|
|
80
|
+
const map = new Map(canonical.map((v) => [v.toLowerCase(), v]));
|
|
81
|
+
return (raw) => {
|
|
82
|
+
const hit = map.get(raw.toLowerCase());
|
|
83
|
+
if (hit === undefined) {
|
|
84
|
+
throw new commander_1.InvalidArgumentError(`Allowed choices are ${canonical.join(", ")} (case-insensitive).`);
|
|
85
|
+
}
|
|
86
|
+
return hit;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
65
89
|
/**
|
|
66
90
|
* 拒绝 CLI 显式覆盖:用于 hideHelp() + .env() 的"沙箱注入参数"(如 --app-id / --branch)。
|
|
67
91
|
*
|
|
@@ -42,6 +42,8 @@ const GRANULARITY_TO_UNIT = {
|
|
|
42
42
|
day: "DAY",
|
|
43
43
|
daily: "DAY",
|
|
44
44
|
"1d": "DAY",
|
|
45
|
+
"1h": "HOUR",
|
|
46
|
+
hour: "HOUR",
|
|
45
47
|
week: "WEEK",
|
|
46
48
|
weekly: "WEEK",
|
|
47
49
|
"1w": "WEEK",
|
|
@@ -72,9 +74,8 @@ async function handleObservabilityAnalytics(opts) {
|
|
|
72
74
|
const timeAggregationUnit = opts.granularity
|
|
73
75
|
? (GRANULARITY_TO_UNIT[opts.granularity] ?? opts.granularity.toUpperCase())
|
|
74
76
|
: "DAY";
|
|
75
|
-
// until 按 timeAggregationUnit 桶向上对齐,避免末桶数据丢失;since 保持原值。
|
|
76
77
|
const nowMs = Date.now();
|
|
77
|
-
const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs -
|
|
78
|
+
const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
|
|
78
79
|
const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
|
|
79
80
|
const bucket = timeAggregationUnit;
|
|
80
81
|
const fieldFilters = (0, helpers_1.buildFieldFilters)([
|
|
@@ -71,7 +71,7 @@ async function handleObservabilityMetric(opts) {
|
|
|
71
71
|
]);
|
|
72
72
|
// since/until 直接以秒透传到 BAM;当前未做桶对齐(如需开启把 until
|
|
73
73
|
// 用 ceilMsToBucket(untilMs, downSample as GranularityBucket) 包一层即可)。
|
|
74
|
-
const downSample = opts.downSample ?? "
|
|
74
|
+
const downSample = opts.downSample ?? "1m";
|
|
75
75
|
const nowMs = Date.now();
|
|
76
76
|
const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
|
|
77
77
|
const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
|
package/dist/utils/http.js
CHANGED
|
@@ -10,6 +10,8 @@ exports.postInnerApi = postInnerApi;
|
|
|
10
10
|
exports.getInnerApi = getInnerApi;
|
|
11
11
|
const http_client_1 = require("@lark-apaas/http-client");
|
|
12
12
|
const error_1 = require("./error");
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const logger_1 = require("./logger");
|
|
13
15
|
let adminClient;
|
|
14
16
|
let runtimeClient;
|
|
15
17
|
/**
|
|
@@ -70,11 +72,31 @@ function applyCanaryHeader(reqConfig) {
|
|
|
70
72
|
}
|
|
71
73
|
/** 走管理端 innerapi 的 POST + 信封解析 */
|
|
72
74
|
async function postInnerApi(url, body, opts) {
|
|
73
|
-
|
|
75
|
+
const startMs = logRequestStart("POST", url, body);
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await getHttpClient().post(url, body);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logResponseFailure("POST", url, err, startMs);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
logResponseEnd("POST", url, response, startMs);
|
|
85
|
+
return handleInnerEnvelope(response, url, opts);
|
|
74
86
|
}
|
|
75
87
|
/** 走管理端 innerapi 的 GET + 信封解析 */
|
|
76
88
|
async function getInnerApi(url, opts) {
|
|
77
|
-
|
|
89
|
+
const startMs = logRequestStart("GET", url);
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await getHttpClient().get(url);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
logResponseFailure("GET", url, err, startMs);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
logResponseEnd("GET", url, response, startMs);
|
|
99
|
+
return handleInnerEnvelope(response, url, opts);
|
|
78
100
|
}
|
|
79
101
|
async function handleInnerEnvelope(response, url, opts) {
|
|
80
102
|
if (!response.ok) {
|
|
@@ -85,6 +107,10 @@ async function handleInnerEnvelope(response, url, opts) {
|
|
|
85
107
|
const env = result;
|
|
86
108
|
if (env.status_code !== undefined && env.status_code !== "0") {
|
|
87
109
|
const msg = env.message ?? env.error_msg ?? "unknown error";
|
|
110
|
+
// verbose: 把完整业务错误信封打到 stderr,便于追溯
|
|
111
|
+
if ((0, config_1.getConfig)().verbose) {
|
|
112
|
+
(0, logger_1.debug)(` envelope: ${truncateForLog(safeStringify(env), 1000)}`);
|
|
113
|
+
}
|
|
88
114
|
const mapped = opts.mapErr?.(env.status_code, msg);
|
|
89
115
|
if (mapped)
|
|
90
116
|
throw mapped;
|
|
@@ -97,3 +123,63 @@ async function handleInnerEnvelope(response, url, opts) {
|
|
|
97
123
|
}
|
|
98
124
|
return result;
|
|
99
125
|
}
|
|
126
|
+
// ── verbose 调试日志 ────────────
|
|
127
|
+
//
|
|
128
|
+
// 仅作用于 inner-api 链路(postInnerApi / getInnerApi),即 app / deploy /
|
|
129
|
+
// observability 三个域。--verbose 关闭时所有 helper 都直接退出,零开销。
|
|
130
|
+
function logRequestStart(method, url, body) {
|
|
131
|
+
const startMs = Date.now();
|
|
132
|
+
if (!(0, config_1.getConfig)().verbose)
|
|
133
|
+
return startMs;
|
|
134
|
+
(0, logger_1.debug)(`→ ${method} ${url}`);
|
|
135
|
+
if (body !== undefined) {
|
|
136
|
+
(0, logger_1.debug)(` body: ${truncateForLog(safeStringify(body), 1000)}`);
|
|
137
|
+
}
|
|
138
|
+
return startMs;
|
|
139
|
+
}
|
|
140
|
+
function logResponseEnd(method, url, response, startMs) {
|
|
141
|
+
if (!(0, config_1.getConfig)().verbose)
|
|
142
|
+
return;
|
|
143
|
+
const elapsedMs = Date.now() - startMs;
|
|
144
|
+
const logid = pickLogid(response.headers);
|
|
145
|
+
const tail = logid ? ` logid=${logid}` : "";
|
|
146
|
+
(0, logger_1.debug)(`← ${method} ${url} ${String(response.status)} ${String(elapsedMs)}ms${tail}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* http-client 在 4xx/5xx / 网络错误时直接 reject(不返回 Response);
|
|
150
|
+
* 这条路径下 logResponseEnd 不会跑——这里专门补一行,尽可能从 HttpError
|
|
151
|
+
* 上抠出 status / logid,让 verbose 仍能看到失败请求的关键信息。
|
|
152
|
+
*/
|
|
153
|
+
function logResponseFailure(method, url, err, startMs) {
|
|
154
|
+
if (!(0, config_1.getConfig)().verbose)
|
|
155
|
+
return;
|
|
156
|
+
const elapsedMs = Date.now() - startMs;
|
|
157
|
+
// @lark-apaas/http-client 的 HttpError.response: Response | undefined
|
|
158
|
+
const e = err;
|
|
159
|
+
const status = e.response?.status;
|
|
160
|
+
const logid = e.response?.headers ? pickLogid(e.response.headers) : null;
|
|
161
|
+
const statusPart = status !== undefined ? String(status) : "ERR";
|
|
162
|
+
const logidPart = logid ? ` logid=${logid}` : "";
|
|
163
|
+
const msgPart = e.message ? ` (${e.message})` : "";
|
|
164
|
+
(0, logger_1.debug)(`✗ ${method} ${url} ${statusPart} ${String(elapsedMs)}ms${logidPart}${msgPart}`);
|
|
165
|
+
}
|
|
166
|
+
/** BAM gateway 在不同 PSM/版本上 logid 落在不同 header;按命中顺序取第一个非空。 */
|
|
167
|
+
function pickLogid(headers) {
|
|
168
|
+
return (headers.get("x-tt-logid") ??
|
|
169
|
+
headers.get("x-logid") ??
|
|
170
|
+
headers.get("logid") ??
|
|
171
|
+
headers.get("x-tt-trace-tag"));
|
|
172
|
+
}
|
|
173
|
+
function safeStringify(v) {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.stringify(v);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return String(v);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function truncateForLog(s, n) {
|
|
182
|
+
if (s.length <= n)
|
|
183
|
+
return s;
|
|
184
|
+
return `${s.slice(0, n)}...(truncated, ${String(s.length)} chars)`;
|
|
185
|
+
}
|