feat(telemetry): expose compute pipeline stage metrics
This commit is contained in:
@@ -57,6 +57,7 @@ function emptyDraft(): ComputeContourDraft {
|
||||
mqtt_host: "127.0.0.1",
|
||||
mqtt_port: 1883,
|
||||
telemetry_poll_interval_seconds: DEFAULT_TELEMETRY_POLL_INTERVAL_SECONDS,
|
||||
mqtt_publish_interval_seconds: 2,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ function draftFromContour(contour: ComputeContour | null): ComputeContourDraft {
|
||||
mqtt_host: contour.mqtt_host,
|
||||
mqtt_port: contour.mqtt_port,
|
||||
telemetry_poll_interval_seconds: contour.telemetry_poll_interval_seconds,
|
||||
mqtt_publish_interval_seconds: contour.mqtt_publish_interval_seconds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,6 +89,9 @@ export function ComputeContourSettingsWindow({
|
||||
const [telemetryPollIntervalDraft, setTelemetryPollIntervalDraft] = useState(
|
||||
() => String(draftFromContour(contour).telemetry_poll_interval_seconds),
|
||||
);
|
||||
const [mqttPublishIntervalDraft, setMqttPublishIntervalDraft] = useState(
|
||||
() => String(draftFromContour(contour).mqtt_publish_interval_seconds),
|
||||
);
|
||||
const [activeSection, setActiveSection] = useState<"connection" | "agent">("connection");
|
||||
const [install, setInstall] = useState<ComputeContourAgentInstall | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -97,6 +102,7 @@ export function ComputeContourSettingsWindow({
|
||||
const nextDraft = draftFromContour(mode === "edit" ? contour : null);
|
||||
setDraft(nextDraft);
|
||||
setTelemetryPollIntervalDraft(String(nextDraft.telemetry_poll_interval_seconds));
|
||||
setMqttPublishIntervalDraft(String(nextDraft.mqtt_publish_interval_seconds));
|
||||
setActiveSection("connection");
|
||||
setInstall(null);
|
||||
setError(null);
|
||||
@@ -121,6 +127,10 @@ export function ComputeContourSettingsWindow({
|
||||
() => parseTelemetryPollIntervalDraft(telemetryPollIntervalDraft),
|
||||
[telemetryPollIntervalDraft],
|
||||
);
|
||||
const mqttPublishIntervalSeconds = useMemo(
|
||||
() => parseTelemetryPollIntervalDraft(mqttPublishIntervalDraft),
|
||||
[mqttPublishIntervalDraft],
|
||||
);
|
||||
|
||||
const valid = useMemo(() => (
|
||||
Boolean(draft.display_name.trim())
|
||||
@@ -130,7 +140,8 @@ export function ComputeContourSettingsWindow({
|
||||
&& Number.isInteger(draft.mqtt_port)
|
||||
&& draft.mqtt_port > 0
|
||||
&& telemetryPollIntervalSeconds !== null
|
||||
), [draft, telemetryPollIntervalSeconds]);
|
||||
&& mqttPublishIntervalSeconds !== null
|
||||
), [draft, mqttPublishIntervalSeconds, telemetryPollIntervalSeconds]);
|
||||
|
||||
const commitTelemetryPollInterval = () => {
|
||||
const resolution = resolveTelemetryPollIntervalDraft(
|
||||
@@ -149,11 +160,34 @@ export function ComputeContourSettingsWindow({
|
||||
));
|
||||
};
|
||||
|
||||
const commitMqttPublishInterval = () => {
|
||||
const resolution = resolveTelemetryPollIntervalDraft(
|
||||
mqttPublishIntervalDraft,
|
||||
draft.mqtt_publish_interval_seconds,
|
||||
);
|
||||
setMqttPublishIntervalDraft(resolution.draft);
|
||||
if (!resolution.accepted) return;
|
||||
setDraft((current) => (
|
||||
current.mqtt_publish_interval_seconds === resolution.seconds
|
||||
? current
|
||||
: {
|
||||
...current,
|
||||
mqtt_publish_interval_seconds: resolution.seconds,
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!valid || busy || telemetryPollIntervalSeconds === null) return;
|
||||
if (
|
||||
!valid
|
||||
|| busy
|
||||
|| telemetryPollIntervalSeconds === null
|
||||
|| mqttPublishIntervalSeconds === null
|
||||
) return;
|
||||
const committedDraft = {
|
||||
...draft,
|
||||
telemetry_poll_interval_seconds: telemetryPollIntervalSeconds,
|
||||
mqtt_publish_interval_seconds: mqttPublishIntervalSeconds,
|
||||
};
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
@@ -273,7 +307,7 @@ export function ComputeContourSettingsWindow({
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
label="Интервал MQTT"
|
||||
label="Обновление интерфейса"
|
||||
hint="1–60 с"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
@@ -296,6 +330,30 @@ export function ComputeContourSettingsWindow({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Интервал MQTT агента"
|
||||
hint="1–60 с · применяется конфигурацией агента"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
aria-invalid={mqttPublishIntervalSeconds === null}
|
||||
value={mqttPublishIntervalDraft}
|
||||
onChange={(event) => setMqttPublishIntervalDraft(event.currentTarget.value)}
|
||||
onBlur={commitMqttPublishInterval}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
commitMqttPublishInterval();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setMqttPublishIntervalDraft(
|
||||
String(draft.mqtt_publish_interval_seconds),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{draft.telemetry_mode === "legacy-ssh" ? (
|
||||
<TextField
|
||||
label="SSH port"
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ComputeContour {
|
||||
mqtt_host: string;
|
||||
mqtt_port: number;
|
||||
telemetry_poll_interval_seconds: number;
|
||||
mqtt_publish_interval_seconds: number;
|
||||
revision: number;
|
||||
updated_at_utc: string | null;
|
||||
}
|
||||
@@ -33,6 +34,7 @@ export interface ComputeContourDraft {
|
||||
mqtt_host: string;
|
||||
mqtt_port: number;
|
||||
telemetry_poll_interval_seconds: number;
|
||||
mqtt_publish_interval_seconds: number;
|
||||
}
|
||||
|
||||
export interface ComputeContourAgentInstall {
|
||||
@@ -99,6 +101,7 @@ export async function createComputeContour(
|
||||
mqtt_host: draft.mqtt_host,
|
||||
mqtt_port: draft.mqtt_port,
|
||||
telemetry_poll_interval_seconds: draft.telemetry_poll_interval_seconds,
|
||||
mqtt_publish_interval_seconds: draft.mqtt_publish_interval_seconds,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface WorkerTelemetryState {
|
||||
}
|
||||
|
||||
export function useWorkerTelemetry(
|
||||
contourId: string | null,
|
||||
pollMilliseconds = DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||
enabled = true,
|
||||
externalRefreshGeneration = 0,
|
||||
@@ -30,7 +31,7 @@ export function useWorkerTelemetry(
|
||||
const refresh = useCallback(() => setGeneration((value) => value + 1), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
if (!enabled || !contourId) {
|
||||
setTelemetry(null);
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
@@ -38,7 +39,7 @@ export function useWorkerTelemetry(
|
||||
}
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
void fetchWorkerTelemetry(controller.signal)
|
||||
void fetchWorkerTelemetry(contourId, controller.signal)
|
||||
.then((document) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setTelemetry(document);
|
||||
@@ -52,7 +53,7 @@ export function useWorkerTelemetry(
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [enabled, externalRefreshGeneration, generation]);
|
||||
}, [contourId, enabled, externalRefreshGeneration, generation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || loading) return;
|
||||
|
||||
@@ -181,13 +181,17 @@ async function requestJson(
|
||||
}
|
||||
|
||||
export async function fetchWorkerTelemetry(
|
||||
contourId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<WorkerTelemetry> {
|
||||
const document = requireJsonRecord(
|
||||
await requestJson("/api/v1/system/worker-telemetry?history=90", {
|
||||
method: "GET",
|
||||
signal,
|
||||
}),
|
||||
await requestJson(
|
||||
`/api/v1/system/contours/${encodeURIComponent(contourId)}/telemetry?history=90`,
|
||||
{
|
||||
method: "GET",
|
||||
signal,
|
||||
},
|
||||
),
|
||||
"missioncore.worker-telemetry/v1",
|
||||
);
|
||||
if (
|
||||
@@ -198,7 +202,7 @@ export async function fetchWorkerTelemetry(
|
||||
|| !isRecord(document.network)
|
||||
|| !Array.isArray(document.history)
|
||||
) {
|
||||
throw new Error("Срез Worker 006 неполон.");
|
||||
throw new Error("Срез вычислительного контура неполон.");
|
||||
}
|
||||
return document as unknown as WorkerTelemetry;
|
||||
}
|
||||
|
||||
@@ -380,7 +380,10 @@
|
||||
.worker-stage-list {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(min(100%, 24rem), 1fr)
|
||||
);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0.48rem;
|
||||
|
||||
@@ -64,10 +64,10 @@ export function ComputeModulesWorkspace() {
|
||||
} = useComputeContours();
|
||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||
const { telemetry, error } = useWorkerTelemetry(
|
||||
selectedContour?.contour_id ?? null,
|
||||
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||
supportsLiveTelemetry,
|
||||
selectedContour !== null,
|
||||
telemetryRefreshGeneration,
|
||||
);
|
||||
const node = telemetry?.node ?? null;
|
||||
|
||||
@@ -22,10 +22,10 @@ export function NetworkWorkspace() {
|
||||
} = useComputeContours();
|
||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||
const { telemetry, error } = useWorkerTelemetry(
|
||||
selectedContour?.contour_id ?? null,
|
||||
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||
supportsLiveTelemetry,
|
||||
selectedContour !== null,
|
||||
telemetryRefreshGeneration,
|
||||
);
|
||||
const aggregate = telemetry?.network.aggregate ?? null;
|
||||
|
||||
@@ -49,7 +49,7 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
||||
assert.match(workspaceHub, /<ComputeModulesWorkspace \/>/);
|
||||
assert.match(workspaceHub, /<NetworkWorkspace \/>/);
|
||||
assert.doesNotMatch(workspaceHub, /worker-telemetry|worker-profile|DESKTOP-OPJ8J04/);
|
||||
assert.match(core, /\/api\/v1\/system\/worker-telemetry/);
|
||||
assert.match(core, /\/api\/v1\/system\/contours\/.*\/telemetry/);
|
||||
assert.match(core, /\/api\/v1\/system\/worker-profile/);
|
||||
assert.match(core, /share_percent/);
|
||||
assert.doesNotMatch(core, /@nodedc\/ui-react/);
|
||||
@@ -68,7 +68,7 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
||||
);
|
||||
assert.match(
|
||||
telemetryStyles,
|
||||
/\.worker-stage-list\s*\{[^}]*grid-template-columns:\s*repeat\(3,/s,
|
||||
/\.worker-stage-list\s*\{[^}]*grid-template-columns:\s*repeat\(\s*auto-fit,\s*minmax\(min\(100%,\s*24rem\),\s*1fr\)/s,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
telemetryStyles,
|
||||
@@ -87,7 +87,8 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
||||
assert.match(contourSettingsHook, /icon: "refresh"/);
|
||||
assert.match(contourSettingsHook, /icon: "settings"/);
|
||||
assert.match(contourSettings, /FieldFrame label="Операционная система"/);
|
||||
assert.match(contourSettings, /label="Интервал MQTT"/);
|
||||
assert.match(contourSettings, /label="Обновление интерфейса"/);
|
||||
assert.match(contourSettings, /label="Интервал MQTT агента"/);
|
||||
assert.match(contourSettings, /value=\{telemetryPollIntervalDraft\}/);
|
||||
assert.match(contourSettings, /onBlur=\{commitTelemetryPollInterval\}/);
|
||||
assert.match(contourSettings, /event\.key === "Escape"/);
|
||||
@@ -96,6 +97,7 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
||||
/telemetry_poll_interval_seconds:\s*Number\(event\.currentTarget\.value\)/,
|
||||
);
|
||||
assert.match(contourContract, /telemetry_poll_interval_seconds: number/);
|
||||
assert.match(contourContract, /mqtt_publish_interval_seconds: number/);
|
||||
assert.match(pollIntervalContract, /MIN_TELEMETRY_POLL_INTERVAL_SECONDS\s*=\s*1/);
|
||||
assert.match(telemetryPolling, /normalizeWorkerTelemetryPollMilliseconds/);
|
||||
assert.match(styles, /system-telemetry\.css/);
|
||||
|
||||
Reference in New Issue
Block a user