You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/06/27 01:30:41 UTC
(skywalking-horizon-ui) 08/18: feat(profiling): validate network task instance has processes before Create
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch refactor/decompose-large-files
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 273433cb5b7325ceb180442e21ed6cf68744e922
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sat Jun 27 00:28:18 2026 +0800
feat(profiling): validate network task instance has processes before Create
The network create modal lists the selected instance's rover-monitored processes (new BFF listProcesses route -> api-client networkProfile.processes) and disables Create with a clear reason when the instance has none. OAP rejects such a task, so this surfaces it upfront instead of failing on submit. Fetch extracted to useNetworkProcesses.
---
apps/bff/src/http/query/ebpf.ts | 43 +++++++++++++++++++
apps/ui/src/api/client.ts | 2 +
apps/ui/src/api/scopes/network-profile.ts | 11 +++++
.../layer/profiling/LayerNetworkProfilingView.vue | 49 ++++++++++++----------
apps/ui/src/layer/profiling/useNetworkProcesses.ts | 47 +++++++++++++++++++++
packages/api-client/src/ebpf.ts | 13 ++++++
packages/api-client/src/index.ts | 2 +
7 files changed, 144 insertions(+), 23 deletions(-)
diff --git a/apps/bff/src/http/query/ebpf.ts b/apps/bff/src/http/query/ebpf.ts
index 5202842..e9a1e40 100644
--- a/apps/bff/src/http/query/ebpf.ts
+++ b/apps/bff/src/http/query/ebpf.ts
@@ -39,6 +39,7 @@ import type {
EBPFTaskCreationResponse,
EBPFTaskListResponse,
FetchLike,
+ NetworkProcessesResponse,
NetworkProfilingCreateRequest,
NetworkProfilingCreateResponse,
NetworkProfilingKeepAliveResponse,
@@ -359,6 +360,15 @@ function relationSeries(env: MqeEnv | undefined): Array<number | null> {
});
}
+const LIST_PROCESSES = /* GraphQL */ `
+ query listNetworkProcesses($instanceId: ID!, $duration: Duration!) {
+ listProcesses(instanceId: $instanceId, duration: $duration) {
+ id
+ name
+ }
+ }
+`;
+
export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): void {
const auth = requireAuth(deps);
@@ -574,6 +584,39 @@ export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): v
},
);
+ /** Processes reporting on a service instance — the network task-creation
+ * modal confirms the instance has profilable processes before Create
+ * (OAP rejects a network task on an instance with none). */
+ app.get(
+ '/api/ebpf/network/processes',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const q = req.query as { serviceInstance?: string; windowMinutes?: string };
+ const instance = (q.serviceInstance ?? '').trim();
+ const payload: NetworkProcessesResponse = { processes: [], reachable: true };
+ if (!instance) return reply.send(payload);
+ const minutes = Math.max(5, Math.min(180, Number(q.windowMinutes) || 30));
+ const endMs = Date.now();
+ const startMs = endMs - minutes * 60_000;
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+ const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
+ try {
+ const data = await graphqlPost<{ listProcesses: NetworkProcessesResponse['processes'] }>(
+ opts,
+ LIST_PROCESSES,
+ {
+ instanceId: instance,
+ duration: { start: fmtMinute(startMs, offset), end: fmtMinute(endMs, offset), step: 'MINUTE' },
+ },
+ );
+ payload.processes = data.listProcesses ?? [];
+ return reply.send(payload);
+ } catch (err) {
+ return reply.send(softErr(payload, err));
+ }
+ },
+ );
+
/** Create a network-profile task on a specific service instance. */
app.post(
'/api/ebpf/network/tasks',
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index aff6ab5..f17c912 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -221,6 +221,8 @@ export type {
ProcessRelationEndpointRef,
ProcessRelationMetric,
ProcessRelationMetricsResponse,
+ NetworkProcess,
+ NetworkProcessesResponse,
NetworkProfilingSampling,
NetworkProfilingCreateRequest,
NetworkProfilingCreateResponse,
diff --git a/apps/ui/src/api/scopes/network-profile.ts b/apps/ui/src/api/scopes/network-profile.ts
index 4db25e5..1944491 100644
--- a/apps/ui/src/api/scopes/network-profile.ts
+++ b/apps/ui/src/api/scopes/network-profile.ts
@@ -17,6 +17,7 @@
import type {
EBPFTaskListResponse,
+ NetworkProcessesResponse,
NetworkProfilingCreateRequest,
NetworkProfilingCreateResponse,
NetworkProfilingKeepAliveResponse,
@@ -65,6 +66,16 @@ export class NetworkProfileApi {
);
}
+ /** Rover-monitored processes on an instance — the create modal confirms
+ * the instance has profilable processes before Create. */
+ processes(serviceInstance: string, windowMinutes = 30): Promise<NetworkProcessesResponse> {
+ const qs = new URLSearchParams({ serviceInstance, windowMinutes: String(windowMinutes) });
+ return this.bff.request<NetworkProcessesResponse>(
+ 'GET',
+ `/api/ebpf/network/processes?${qs.toString()}`,
+ );
+ }
+
create(body: NetworkProfilingCreateRequest): Promise<NetworkProfilingCreateResponse> {
return this.bff.request<NetworkProfilingCreateResponse>(
'POST',
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index fdfef08..5904388 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -47,6 +47,7 @@ import type {
ProcessRelationMetricsResponse,
} from '@/api/client';
import ProcessTopologyGraph from '@/layer/profiling/ProcessTopologyGraph.vue';
+import { useNetworkProcesses } from '@/layer/profiling/useNetworkProcesses';
import TimeChart from '@/components/charts/TimeChart.vue';
import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
import Icon from '@/components/icons/Icon.vue';
@@ -58,9 +59,7 @@ const layerKey = computed(() => String(route.params.layerKey ?? ''));
const previewProcessTopology = usePreviewLayerBlock(layerKey, 'processTopology');
const { selectedId: serviceId } = useSelectedService();
-// Instance picker (binds to ?serviceInstance= via plain ref state — the
-// network view needs an *instance* to be useful, so we don't reuse the
-// generic `useSelectedInstance` composable's URL contract).
+// Instance picker — plain ref state (not the URL-bound useSelectedInstance).
const instances = useLayerInstances(layerKey, serviceId);
const selectedInstanceId = ref<string | null>(null);
watch(
@@ -79,12 +78,8 @@ const tasksError = ref<string | null>(null);
const tasksLoading = ref(false);
const currentTask = ref<EBPFTask | null>(null);
-// Tasks are listed per SERVICE, not per selected instance. A network
-// task runs against the instance that was live when it was created; if
-// that pod has since been replaced (new pod name), AND-ing the task
-// query with the currently-selected instance hides the task entirely.
-// The task object carries its own serviceInstanceId, so the list stays
-// correct and selecting a task can still drive the right topology.
+// Tasks are listed per SERVICE, not the selected instance — a task's pod
+// may have been replaced; the task carries its own serviceInstanceId.
watch(
() => layerKey.value + '|' + (serviceId.value ?? ''),
() => void refreshTasks(),
@@ -122,11 +117,8 @@ const topologyLoading = ref(false);
const topologyError = ref<string | null>(null);
const windowMinutes = ref(30);
-// Topology follows the SELECTED TASK: a finished FIXED_TIME task only
-// captured process relations on its own instance during its own window
-// (and that pod may since have been replaced). When a task is selected
-// we query its instance + [taskStartTime, taskStartTime+duration]; with
-// no task we fall back to the picker instance + rolling window.
+// Topology follows the selected task (its instance + capture window); with
+// no task, the picker instance + a rolling window.
watch([currentTask, selectedInstanceId], () => void loadTopology());
async function loadTopology(): Promise<void> {
@@ -187,9 +179,7 @@ function endpointRef(n: ProcessNode): ProcessRelationEndpointRef {
};
}
-// Refire when the operator clicks a different edge. The detail area
-// resets first (cascade-clear), then resolves async — never leaves a
-// stale conversation's numbers under the new edge's header.
+// Refire on a new edge: reset first (cascade-clear), then resolve async.
watch(selectedCall, async (call) => {
relationMetrics.value = null;
relationError.value = null;
@@ -292,11 +282,11 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onEdgeKeydown));
const showNewTask = ref(false);
useEscapeToClose(() => showNewTask.value, () => (showNewTask.value = false));
const newTaskError = ref<string | null>(null);
+const { processes: networkProcesses, loading: processesLoading } =
+ useNetworkProcesses(selectedInstanceId, showNewTask);
const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
-// OAP's `EBPFNetworkDataCollectingSettings.requireCompleteRequest` and
-// `requireCompleteResponse` are `Boolean!` — non-null. Every sampling
-// row MUST carry the settings block, otherwise
-// `createEBPFNetworkProfiling` 400s with a schema validation error.
+// requireComplete{Request,Response} are Boolean! — every sampling row must
+// carry the settings block or create 400s.
const DEFAULT_SETTINGS = (): NetworkProfilingSampling['settings'] => ({
requireCompleteRequest: true,
requireCompleteResponse: true,
@@ -497,6 +487,16 @@ function fmtTime(ms: number): string {
<div v-if="!instances.instances.value.length" class="banner err">
No instances available for this service — a network profile task cannot be created.
</div>
+ <template v-else-if="selectedInstanceId">
+ <div v-if="processesLoading" class="hint">Checking processes on this instance…</div>
+ <div v-else-if="!networkProcesses.length" class="banner err">
+ This instance has no profilable processes — a network task cannot be created (network profiling needs a rover-monitored process).
+ </div>
+ <div v-else class="proc-list">
+ <span class="proc-list-label">Processes ({{ networkProcesses.length }})</span>
+ <span v-for="p in networkProcesses" :key="p.id" class="proc-chip">{{ p.name }}</span>
+ </div>
+ </template>
<p class="hint">
OAP captures one connection sample per matching rule. Leave URI
empty to match any request; toggle 4xx/5xx to scope by status.
@@ -535,8 +535,8 @@ function fmtTime(ms: number): string {
<button class="btn-secondary" @click="showNewTask = false">Cancel</button>
<button
class="btn-primary"
- :disabled="!selectedInstanceId"
- :title="selectedInstanceId ? 'Create the network profile task' : 'No instances available for this service'"
+ :disabled="!selectedInstanceId || processesLoading || !networkProcesses.length"
+ :title="!selectedInstanceId ? 'No instances available for this service' : processesLoading ? 'Checking processes…' : !networkProcesses.length ? 'This instance has no profilable processes' : 'Create the network profile task'"
@click="submitNewTask"
>Create task</button>
</div>
@@ -993,4 +993,7 @@ function fmtTime(ms: number): string {
color: var(--sw-err);
font-size: 11px;
}
+.proc-list { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 6px 0; }
+.proc-list-label { font-size: 11px; color: var(--sw-fg-3); }
+.proc-chip { font-family: var(--sw-mono); font-size: 10.5px; color: var(--sw-fg-1); background: var(--sw-bg-2); border: 1px solid var(--sw-line); padding: 1px 6px; border-radius: 3px; }
</style>
diff --git a/apps/ui/src/layer/profiling/useNetworkProcesses.ts b/apps/ui/src/layer/profiling/useNetworkProcesses.ts
new file mode 100644
index 0000000..94713a7
--- /dev/null
+++ b/apps/ui/src/layer/profiling/useNetworkProcesses.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ref, watch, type Ref } from 'vue';
+import { bffClient } from '@/api/client';
+import type { NetworkProcess } from '@/api/client';
+
+/**
+ * Processes reporting on a service instance. A network profiling task needs
+ * at least one (OAP rejects an instance with none), so the create modal
+ * fetches them lazily while `enabled` to validate + show them before Create.
+ */
+export function useNetworkProcesses(instanceId: Ref<string | null>, enabled: Ref<boolean>) {
+ const processes = ref<NetworkProcess[]>([]);
+ const loading = ref(false);
+ watch([enabled, instanceId], async ([on, id]) => {
+ if (!on || !id) {
+ processes.value = [];
+ return;
+ }
+ loading.value = true;
+ try {
+ const resp = await bffClient.networkProfile.processes(id);
+ if (!enabled.value || instanceId.value !== id) return; // stale
+ processes.value = resp.processes ?? [];
+ } catch {
+ if (enabled.value && instanceId.value === id) processes.value = [];
+ } finally {
+ if (enabled.value && instanceId.value === id) loading.value = false;
+ }
+ });
+ return { processes, loading };
+}
diff --git a/packages/api-client/src/ebpf.ts b/packages/api-client/src/ebpf.ts
index bfbe6cc..5400946 100644
--- a/packages/api-client/src/ebpf.ts
+++ b/packages/api-client/src/ebpf.ts
@@ -159,6 +159,19 @@ export interface ProcessTopologyResponse {
error?: string;
}
+/** A rover-monitored process on a service instance. Network profiling
+ * targets the whole instance; this list confirms it has profilable
+ * processes (OAP rejects a network task on an instance with none). */
+export interface NetworkProcess {
+ id: string;
+ name: string;
+}
+export interface NetworkProcessesResponse {
+ processes: NetworkProcess[];
+ reachable: boolean;
+ error?: string;
+}
+
export interface NetworkProfilingSampling {
uriRegex?: string;
when4xx?: boolean;
diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts
index 8c6fadb..2ef5a1a 100644
--- a/packages/api-client/src/index.ts
+++ b/packages/api-client/src/index.ts
@@ -187,6 +187,8 @@ export type {
ProcessNode,
ProcessCall,
ProcessTopologyResponse,
+ NetworkProcess,
+ NetworkProcessesResponse,
NetworkProfilingSampling,
NetworkProfilingCreateRequest,
NetworkProfilingCreateResponse,