fix(web-probe): expose slow API samples in observe analysis (#644)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -139,6 +139,7 @@ interface SearchOptions extends CommonOptions {
|
||||
limit: number;
|
||||
candidateLimit: number;
|
||||
lookbackMinutes: number;
|
||||
endAt: string | null;
|
||||
}
|
||||
|
||||
interface DiagnoseCodeAgentOptions extends CommonOptions {
|
||||
@@ -273,6 +274,7 @@ function parseSearchOptions(args: string[]): SearchOptions {
|
||||
let limit = 20;
|
||||
let candidateLimit = 80;
|
||||
let lookbackMinutes = 360;
|
||||
let endAt: string | null = null;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--grep") {
|
||||
@@ -319,6 +321,13 @@ function parseSearchOptions(args: string[]): SearchOptions {
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) throw new Error(`${arg} must be an integer from 1 to 10080`);
|
||||
lookbackMinutes = parsed;
|
||||
index += 1;
|
||||
} else if (arg === "--end-at") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--end-at requires an ISO timestamp value");
|
||||
const parsed = Date.parse(value);
|
||||
if (!Number.isFinite(parsed)) throw new Error("--end-at must be an ISO timestamp parseable by Date.parse");
|
||||
endAt = new Date(parsed).toISOString();
|
||||
index += 1;
|
||||
} else {
|
||||
commonArgs.push(arg);
|
||||
if (arg === "--target") {
|
||||
@@ -328,7 +337,7 @@ function parseSearchOptions(args: string[]): SearchOptions {
|
||||
}
|
||||
}
|
||||
if (grep === null && query === null && path === null && status === null) throw new Error("observability search requires --grep <text>, --query <tempo-query>, --path <route>, or --status <code>");
|
||||
return { ...parseCommonOptions(commonArgs), grep, query, path, status, limit, candidateLimit, lookbackMinutes };
|
||||
return { ...parseCommonOptions(commonArgs), grep, query, path, status, limit, candidateLimit, lookbackMinutes, endAt };
|
||||
}
|
||||
|
||||
function parseDiagnoseCodeAgentOptions(args: string[]): DiagnoseCodeAgentOptions {
|
||||
@@ -687,7 +696,9 @@ async function search(config: UniDeskConfig, options: SearchOptions): Promise<Re
|
||||
query: effectiveTempoQuery,
|
||||
lookbackMinutes: effectiveLookbackMinutes,
|
||||
};
|
||||
const endSeconds = Math.floor(Date.now() / 1000);
|
||||
const endMillis = options.endAt === null ? Date.now() : Date.parse(options.endAt);
|
||||
if (!Number.isFinite(endMillis)) throw new Error("--end-at must be an ISO timestamp parseable by Date.parse");
|
||||
const endSeconds = Math.floor(endMillis / 1000);
|
||||
const startSeconds = endSeconds - (effectiveLookbackMinutes * 60);
|
||||
const params = new URLSearchParams({
|
||||
start: String(startSeconds),
|
||||
@@ -710,6 +721,7 @@ async function search(config: UniDeskConfig, options: SearchOptions): Promise<Re
|
||||
statusFilter: options.status,
|
||||
lookbackMinutes: effectiveLookbackMinutes,
|
||||
requestedLookbackMinutes: options.lookbackMinutes,
|
||||
endAt: new Date(endSeconds * 1000).toISOString(),
|
||||
businessTraceId,
|
||||
mode: businessTraceId === null ? "candidate-grep" : "business-trace-exact",
|
||||
candidateLimit: options.candidateLimit,
|
||||
@@ -882,23 +894,30 @@ function renderSearchTable(input: {
|
||||
result: Record<string, unknown>;
|
||||
}): RenderedCliResult {
|
||||
const traces = asArray(input.result.traces).map((item) => asPlainRecord(item) ?? {});
|
||||
const rows = traces.slice(0, 12).map((trace) => [
|
||||
shortenMiddle(textValue(trace.traceId), 20),
|
||||
searchTraceStatus(trace),
|
||||
numberValue(trace.spanCount),
|
||||
numberValue(trace.errorSpanCount),
|
||||
numberValue(trace.matchedSpanCount),
|
||||
joinValues(trace.services, 46),
|
||||
]);
|
||||
const requestedLimit = numberFromUnknown(input.options.limit);
|
||||
const rowLimit = Math.max(1, Math.min(Number.isFinite(requestedLimit) ? Math.round(requestedLimit) : 12, 40));
|
||||
const rows = traces.slice(0, rowLimit).map((trace) => {
|
||||
const meta = asPlainRecord(trace.meta) ?? {};
|
||||
return [
|
||||
shortenMiddle(textValue(trace.traceId), 32),
|
||||
shortenEnd(textValue(trace.startAt ?? isoFromUnixNano(meta.startTimeUnixNano)), 20),
|
||||
numberValue(trace.durationMs ?? meta.durationMs),
|
||||
searchTraceStatus(trace),
|
||||
numberValue(trace.spanCount),
|
||||
numberValue(trace.errorSpanCount),
|
||||
numberValue(trace.matchedSpanCount),
|
||||
joinValues(trace.services, 38),
|
||||
];
|
||||
});
|
||||
const lines = [
|
||||
`platform-infra observability search (${input.ok ? "ok" : "not-ok"})`,
|
||||
"",
|
||||
formatTable(["TRACE", "STATUS", "SPANS", "ERR", "MATCH", "SERVICES"], rows.length > 0 ? rows : [["-", "no-match", "-", "-", "-", "-"]]),
|
||||
formatTable(["TRACE", "START", "DUR_MS", "STATUS", "SPANS", "ERR", "MATCH", "SERVICES"], rows.length > 0 ? rows : [["-", "-", "-", "no-match", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` target=${input.target.id} backend=${textValue(input.query.backend)} service=${textValue(input.query.service)}`,
|
||||
` grep=${textValue(input.query.grep)} path=${textValue(input.query.pathFilter)} status=${textValue(input.query.statusFilter)} tempoQuery=${shortenMiddle(textValue(input.query.tempoQuery), 80)}`,
|
||||
` mode=${textValue(input.query.mode)} businessTraceId=${textValue(input.query.businessTraceId)} lookbackMinutes=${textValue(input.query.lookbackMinutes)} requestedLookbackMinutes=${textValue(input.query.requestedLookbackMinutes)}`,
|
||||
` mode=${textValue(input.query.mode)} businessTraceId=${textValue(input.query.businessTraceId)} lookbackMinutes=${textValue(input.query.lookbackMinutes)} requestedLookbackMinutes=${textValue(input.query.requestedLookbackMinutes)} endAt=${textValue(input.query.endAt)}`,
|
||||
` candidateLimit=${textValue(input.query.candidateLimit)} limit=${textValue(input.query.limit)}`,
|
||||
` candidates=${textValue(input.result.candidateTraceCount)} scanned=${textValue(input.result.scannedTraceCount)} matched=${textValue(input.result.matchedTraceCount)} stopped=${textValue(input.result.scanStopped)}`,
|
||||
];
|
||||
@@ -1090,6 +1109,7 @@ function buildSearchCommand(target: ObservabilityTarget, options: SearchOptions,
|
||||
if (options.query !== null) parts.push("--query", options.query);
|
||||
if (options.path !== null) parts.push("--path", options.path);
|
||||
if (options.status !== null) parts.push("--status", String(options.status));
|
||||
if (options.endAt !== null) parts.push("--end-at", options.endAt);
|
||||
parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit));
|
||||
if (full) parts.push("--full");
|
||||
return parts.map(cliArg).join(" ");
|
||||
@@ -1138,6 +1158,19 @@ function numberFromUnknown(value: unknown): number {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
function isoFromUnixNano(value: unknown): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
try {
|
||||
const text = String(value);
|
||||
if (!/^\d+$/u.test(text)) return "-";
|
||||
const millis = Number(BigInt(text) / 1000000n);
|
||||
if (!Number.isFinite(millis) || millis <= 0) return "-";
|
||||
return new Date(millis).toISOString().replace(/\.000Z$/u, "Z");
|
||||
} catch {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
function joinValues(value: unknown, maxLength: number): string {
|
||||
const joined = asArray(value).map(textValue).filter((item) => item !== "-").join(",");
|
||||
return shortenEnd(joined.length > 0 ? joined : textValue(value), maxLength);
|
||||
@@ -1188,11 +1221,15 @@ function compactSearchResult(value: unknown): Record<string, unknown> {
|
||||
|
||||
function compactSearchTrace(value: unknown): Record<string, unknown> {
|
||||
const trace = asPlainRecord(value) ?? {};
|
||||
const meta = compactMetaRecord(trace.meta);
|
||||
const startTimeUnixNano = trace.startTimeUnixNano ?? meta?.startTimeUnixNano ?? null;
|
||||
return {
|
||||
traceId: trace.traceId ?? null,
|
||||
startAt: isoFromUnixNano(startTimeUnixNano),
|
||||
durationMs: trace.durationMs ?? meta?.durationMs ?? null,
|
||||
traceCommand: trace.traceCommand ?? null,
|
||||
grepCommand: trace.grepCommand ?? null,
|
||||
meta: compactMetaRecord(trace.meta),
|
||||
meta,
|
||||
queryOk: trace.queryOk ?? null,
|
||||
parseOk: trace.parseOk ?? null,
|
||||
rawMatched: trace.rawMatched ?? null,
|
||||
|
||||
Reference in New Issue
Block a user