Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions frontend/providers/applaunchpad/data/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ launchpad:
enabled: false
fastGPTKey: ''
components:
monitor:
url: http://launchpad-monitor.sealos.svc.cluster.local:8428
metrics:
url: http://vmselect-vm-stack-victoria-metrics-k8s-stack.vm.svc.cluster.local:8481/select/0/prometheus
whitelistKubernetesHosts: []
billing:
url: 'http://account-service.account-system.svc:2333'
log:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ data:
domain: {{ .cloudDomain }}
port: {{ if .cloudPort }}:{{ .cloudPort }}{{ end }}
desktopDomain: {{ .cloudDomain }}
userDomains:
userDomains:
- name: {{ .cloudDomain }}
secretName: {{ .certSecretName }}
common:
Expand All @@ -32,8 +32,9 @@ data:
enabled: false
fastGPTKey: ""
components:
monitor:
url: {{ .monitorUrl }}
metrics:

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deployment template uses {{ default "..." .metricsUrl }} which provides a sensible default. However, ensure that the .metricsUrl variable is documented in the deployment instructions or README, as this is a new configuration parameter that replaces the old .monitorUrl. Consider adding migration documentation for users upgrading from the old monitor configuration to the new metrics configuration.

Suggested change
metrics:
metrics:
# metricsUrl is the configurable endpoint for the metrics backend.
# It replaces the legacy monitorUrl parameter; when migrating from older
# configurations that used monitorUrl, set metricsUrl to the same value.
# If metricsUrl is not provided, the following in-cluster VictoriaMetrics
# endpoint is used by default.

Copilot uses AI. Check for mistakes.
url: {{ default "http://vmselect-vm-stack-victoria-metrics-k8s-stack.vm.svc.cluster.local:8481/select/0/prometheus" .metricsUrl }}
whitelistKubernetesHosts: []
billing:
url: {{ .billingUrl }}
log:
Expand Down
3 changes: 2 additions & 1 deletion frontend/providers/applaunchpad/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"react-syntax-highlighter": "^15.5.0",
"sass": "^1.68.0",
"sealos-desktop-sdk": "workspace:*",
"sealos-metrics-sdk": "workspace:*",
"typescript": "^5.2.2",
"zod": "^3.23.8",
"zod-openapi": "^4.2.4",
Expand Down Expand Up @@ -96,4 +97,4 @@
"vitest": "^4.0.12",
"webpack-bundle-analyzer": "^4.9.1"
}
}
}
4 changes: 4 additions & 0 deletions frontend/providers/applaunchpad/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export async function register() {
monitor: {
url: 'http://launchpad-monitor.sealos.svc.cluster.local:8428'
},
metrics: {
url: 'http://vmselect-vm-stack-victoria-metrics-k8s-stack.vm.svc.cluster.local:8481/select/0/prometheus',
whitelistKubernetesHosts: []
},
billing: {
url: 'http://account-service.account-system.svc:2333'
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,46 @@ import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { ApiResp } from '@/services/kubernet';
import { monitorFetch } from '@/services/monitorFetch';
import { MonitorServiceResult, MonitorDataResult, MonitorQueryKey } from '@/types/monitor';
import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor';
import type { NextApiRequest, NextApiResponse } from 'next';
import type {
LaunchpadMetricType,
LaunchpadQueryParams,
LaunchpadQueryResult
} from 'sealos-metrics-sdk';

const normalizeQueryParam = (value: string | string[] | undefined) =>
Array.isArray(value) ? value[0] : value;
Comment on lines +14 to +15

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizeQueryParam function only handles string and string[] types but doesn't handle the case where the value is undefined. While TypeScript shows this is acceptable via the signature string | string[] | undefined, the function should explicitly handle undefined by returning it directly. Currently, if value is undefined, it will return undefined correctly due to the implicit else case, but this could be more explicit for better code clarity.

Suggested change
const normalizeQueryParam = (value: string | string[] | undefined) =>
Array.isArray(value) ? value[0] : value;
const normalizeQueryParam = (value: string | string[] | undefined) => {
if (value === undefined) return value;
return Array.isArray(value) ? value[0] : value;
};

Copilot uses AI. Check for mistakes.

const adaptChartData = (
data: LaunchpadQueryResult,
metricName: 'pod' | 'persistentvolumeclaim'
): MonitorDataResult[] =>
data.data.result.map((item) => ({
name: item.metric[metricName] || item.metric.pod || item.metric.persistentvolumeclaim,
xData: item.values?.map((value) => Number(value[0])) ?? [],
yData: item.values?.map((value) => Number.parseFloat(value[1]).toFixed(2)) ?? []

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adaptChartData function uses Number.parseFloat(value[1]).toFixed(2) which returns a string. Since yData is defined as string[] in the MonitorDataResult type, this is technically correct. However, the original code used parseFloat(value[1]).toFixed(2) which has the same behavior. The change to Number.parseFloat is fine, but ensure this is intentional as it adds an extra level of namespace access without functional benefit.

Copilot uses AI. Check for mistakes.
}));

Comment on lines +21 to 26

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic in adaptChartData (line 22) uses item.metric[metricName] || item.metric.pod || item.metric.persistentvolumeclaim to ensure a name is always provided. This is good defensive programming. However, ensure that this fallback is tested, as it could mask issues where the expected metric name is missing. Consider logging a warning when falling back to avoid silent failures in production.

Suggested change
data.data.result.map((item) => ({
name: item.metric[metricName] || item.metric.pod || item.metric.persistentvolumeclaim,
xData: item.values?.map((value) => Number(value[0])) ?? [],
yData: item.values?.map((value) => Number.parseFloat(value[1]).toFixed(2)) ?? []
}));
data.data.result.map((item) => {
const primaryName = item.metric[metricName];
const fallbackName = item.metric.pod || item.metric.persistentvolumeclaim;
const name = primaryName || fallbackName || 'unknown';
if (!primaryName && fallbackName) {
console.warn(
`[monitor] adaptChartData: missing expected metric "${metricName}" in item.metric, using fallback name "${fallbackName}".`
);
} else if (!primaryName && !fallbackName) {
console.warn(
`[monitor] adaptChartData: missing "${metricName}", "pod", and "persistentvolumeclaim" in item.metric; using "unknown" as name.`
);
}
return {
name,
xData: item.values?.map((value) => Number(value[0])) ?? [],
yData: item.values?.map((value) => Number.parseFloat(value[1]).toFixed(2)) ?? []
};
});

Copilot uses AI. Check for mistakes.
const AdapterChartData: Record<
keyof MonitorQueryKey,
(data: MonitorServiceResult) => MonitorDataResult[]
(data: LaunchpadQueryResult) => MonitorDataResult[]
> = {
disk: (data: MonitorServiceResult) => {
const newDataArray = data.data.result.map((item) => {
let name = item.metric.pod;
let xData = item.values.map((value) => value[0]);
let yData = item.values.map((value) => parseFloat(value[1]).toFixed(2));
return {
name: name,
xData: xData,
yData: yData
};
});
return newDataArray;
},
cpu: (data: MonitorServiceResult) => {
const newDataArray = data.data.result.map((item) => {
let name = item.metric.pod;
let xData = item.values.map((value) => value[0]);
let yData = item.values.map((value) => parseFloat(value[1]).toFixed(2));
return {
name: name,
xData: xData,
yData: yData
};
});
return newDataArray;
},
memory: (data: MonitorServiceResult) => {
const newDataArray = data.data.result.map((item) => {
let name = item.metric.pod;
let xData = item.values.map((value) => value[0]);
let yData = item.values.map((value) => parseFloat(value[1]).toFixed(2));
return {
name: name,
xData: xData,
yData: yData
};
});
return newDataArray;
},
average_cpu: (data: MonitorServiceResult) => {
const newDataArray = data.data.result.map((item) => {
let name = item.metric.pod;
let xData = item.values.map((value) => value[0]);
let yData = item.values.map((value) => parseFloat(value[1]).toFixed(2));
return {
name: name,
xData: xData,
yData: yData
};
});
return newDataArray;
},
average_memory: (data: MonitorServiceResult) => {
const newDataArray = data.data.result.map((item) => {
let name = item.metric.pod;
let xData = item.values.map((value) => value[0]);
let yData = item.values.map((value) => parseFloat(value[1]).toFixed(2));
return {
name: name,
xData: xData,
yData: yData
};
});
return newDataArray;
},
storage: (data: MonitorServiceResult) => {
const newDataArray = data.data.result.map((item) => {
let name = item.metric.persistentvolumeclaim;
let xData = item.values.map((value) => value[0]);
let yData = item.values.map((value) => parseFloat(value[1]).toFixed(2));
return {
name: name,
xData: xData,
yData: yData
};
});
return newDataArray;
}
disk: (data) => adaptChartData(data, 'persistentvolumeclaim'),
cpu: (data) => adaptChartData(data, 'pod'),
memory: (data) => adaptChartData(data, 'pod'),
average_cpu: (data) => adaptChartData(data, 'pod'),
average_memory: (data) => adaptChartData(data, 'pod'),
storage: (data) => adaptChartData(data, 'persistentvolumeclaim')
};

const queryTypeMap: Record<keyof MonitorQueryKey, LaunchpadMetricType> = {
cpu: 'cpu',
memory: 'memory',
disk: 'disk',
average_cpu: 'average_cpu',
average_memory: 'average_memory',
storage: 'disk'
Comment on lines +39 to +45

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queryTypeMap maps storage to disk, but the SDK's LaunchpadMetricType may not distinguish between these two. Verify that the SDK correctly handles the disk type for storage queries, especially when a pvcName is provided. The mapping seems correct based on line 45 where both use disk, but ensure this is documented in the SDK.

Copilot uses AI. Check for mistakes.
};

const alignMonitorData = (dataArray: MonitorDataResult[]): MonitorDataResult[] => {
Expand Down Expand Up @@ -115,37 +70,47 @@ const alignMonitorData = (dataArray: MonitorDataResult[]): MonitorDataResult[] =
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const kubeconfig = await authSession(req.headers);
const { namespace, kc } = await getK8s({
const { namespace } = await getK8s({
kubeconfig: kubeconfig
});

const { queryName, queryKey, start, end, step = '1m', pvcName } = req.query;
const normalizedQueryName = normalizeQueryParam(queryName as string | string[] | undefined);
const normalizedQueryKey = normalizeQueryParam(queryKey as string | string[] | undefined) as
| keyof MonitorQueryKey
| undefined;
const normalizedStep = normalizeQueryParam(step as string | string[] | undefined) || '1m';
const normalizedPvcName = normalizeQueryParam(pvcName as string | string[] | undefined);

if (!normalizedQueryName || !normalizedQueryKey || !queryTypeMap[normalizedQueryKey]) {
return jsonRes(res, {
code: 400,
message: 'Invalid monitor query params'
});
}

// One hour of monitoring data
const endTime = end ? Number(end) : Date.now();
const startTime = start ? Number(start) : endTime - 60 * 60 * 1000;
const endTime = end
? Number(normalizeQueryParam(end as string | string[] | undefined))
: Date.now();
const startTime = start
? Number(normalizeQueryParam(start as string | string[] | undefined))
: endTime - 60 * 60 * 1000;

const params = {
type: queryKey,
launchPadName: queryName,
const params: LaunchpadQueryParams = {
type: queryTypeMap[normalizedQueryKey],
podName: normalizedQueryName,
namespace: namespace,
start: Math.floor(startTime / 1000),
end: Math.floor(endTime / 1000),
step: step,
...(pvcName && { pvcName: pvcName })
range: {
start: Math.floor(startTime / 1000),
end: Math.floor(endTime / 1000),
step: normalizedStep
},
...(normalizedPvcName && { pvcName: normalizedPvcName })
};

const result: MonitorDataResult[] = await monitorFetch(
{
url: '/query',
params: params
},
kubeconfig
).then((res) => {
const key = queryKey as keyof MonitorQueryKey;
const adaptedData = AdapterChartData[key]
? AdapterChartData[key](res as MonitorServiceResult)
: (res as any);
const result: MonitorDataResult[] = await monitorFetch(params, kubeconfig).then((sdkRes) => {
const adaptedData = AdapterChartData[normalizedQueryKey](sdkRes as LaunchpadQueryResult);

return alignMonitorData(adaptedData);
});
Expand Down
36 changes: 10 additions & 26 deletions frontend/providers/applaunchpad/src/services/monitorFetch.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
import { AxiosRequestConfig } from 'axios';
import { MetricsClient } from 'sealos-metrics-sdk';
import type { LaunchpadQueryParams } from 'sealos-metrics-sdk';

export const monitorFetch = async (props: AxiosRequestConfig, kubeconfig: string) => {
const { url, params } = props;
const queryString = typeof params === 'object' ? new URLSearchParams(params).toString() : params;
const requestOptions = {
method: 'GET',
headers: {
Authorization: encodeURIComponent(kubeconfig)
}
};
const doMain =
global.AppConfig.launchpad.components.monitor.url ||
'http://launchpad-monitor.sealos.svc.cluster.local:8428';
export const monitorFetch = async (params: LaunchpadQueryParams, kubeconfig: string) => {
const metricsConfig = global.AppConfig?.launchpad?.components?.metrics;
const client = new MetricsClient({
kubeconfig,
metricsURL: metricsConfig?.url,
whitelistKubernetesHosts: metricsConfig?.whitelistKubernetesHosts
Comment on lines +6 to +9

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metricsConfig?.url uses optional chaining which is good for safety. However, if both metricsConfig?.url and metricsConfig?.whitelistKubernetesHosts are undefined, the SDK will use its default values. Consider validating that the metrics configuration is properly loaded at application startup to catch configuration errors early, rather than discovering them when the first monitoring request is made.

Suggested change
const client = new MetricsClient({
kubeconfig,
metricsURL: metricsConfig?.url,
whitelistKubernetesHosts: metricsConfig?.whitelistKubernetesHosts
if (!metricsConfig || (metricsConfig.url === undefined && metricsConfig.whitelistKubernetesHosts === undefined)) {
throw new Error('Metrics configuration for launchpad is missing or invalid: expected metrics.url or metrics.whitelistKubernetesHosts to be defined.');
}
const client = new MetricsClient({
kubeconfig,
metricsURL: metricsConfig.url,
whitelistKubernetesHosts: metricsConfig.whitelistKubernetesHosts

Copilot uses AI. Check for mistakes.
});

try {
const response = await fetch(`${doMain}${url}?${queryString}`, requestOptions);

if (!response.ok) {
throw new Error(`Error monitorFetch ${response.status}`);
}
return await response.json();
} catch (error) {
if (process.env.NODE_ENV === 'development') {
throw new Error('Please check if monitor service api is configured:');
}
throw error;
}
return client.launchpad.query(params);

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new monitorFetch function removes all explicit error handling that was present in the original implementation. The original code had a try-catch block with environment-specific error messages. While the SDK may handle errors internally, consider wrapping the SDK call in a try-catch block to provide more context-specific error messages for debugging, especially during development. This will help identify whether issues are with the SDK configuration, network connectivity, or authentication.

Suggested change
return client.launchpad.query(params);
try {
return await client.launchpad.query(params);
} catch (error) {
if (process.env.NODE_ENV !== 'production') {
// Provide more context during development to help debug SDK, network, or auth issues
// Do not change the error shape; just add logging.
// eslint-disable-next-line no-console
console.error('monitorFetch: failed to query launchpad metrics', {
error,
metricsURL: metricsConfig?.url
});
}
throw error;
}

Copilot uses AI. Check for mistakes.
};
3 changes: 2 additions & 1 deletion frontend/providers/applaunchpad/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ export type AppConfigType = {
fastGPTKey?: string;
};
components: {
monitor: {
metrics: {
url: string;
whitelistKubernetesHosts: string[];
};
billing: {
url: string;
Expand Down
35 changes: 0 additions & 35 deletions frontend/providers/applaunchpad/src/types/monitor.d.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,3 @@
import { BoxProps } from '@chakra-ui/react';

export interface MonitorServiceResult {
status: string;
data: {
resultType: string;
result: {
metric: {
app_kubernetes_io_instance: string;
app_kubernetes_io_managed_by: string;
app_kubernetes_io_name: string;
apps_kubeblocks_io_component_name: string;
datname: string;
instance: string;
job: string;
namespace: string;
node: string;
pod: string;
server: string;
service: string;
__name__: string;
state?: string;
command?: string;
database?: string;
db: string;
type?: string;
cmd?: string;
persistentvolumeclaim?: string;
};
value: [number, string];
values: [[number, string]];
}[];
};
}

export type MonitorQueryKey = {
cpu: string;
memory: string;
Expand Down
Loading