feat(telemetry): expose compute pipeline stage metrics

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 01:53:31 +03:00
parent 783aa444ac
commit 79eb4b46f7
19 changed files with 1000 additions and 24 deletions
@@ -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="160 с"
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/);