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 <[email protected]> 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,
