feat(fleet): show instance resource metrics
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import type { InstanceService } from '../instances/instance-service.js';
|
||||
import type {
|
||||
InstanceSessionStore,
|
||||
UpstreamResponse,
|
||||
UpstreamSessionClientOptions,
|
||||
} from '../connections/upstream-session-client.js';
|
||||
|
||||
export interface InstanceResources {
|
||||
readonly cpuPercent?: number;
|
||||
readonly memoryPercent?: number;
|
||||
readonly maxTemperatureCelsius?: number;
|
||||
readonly phoneNumbers?: readonly string[];
|
||||
}
|
||||
|
||||
const MAX_BODY_BYTES = 32_768;
|
||||
const record = (value: unknown): Record<string, unknown> | undefined =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
const ranged = (value: unknown, minimum: number, maximum: number): number | undefined =>
|
||||
typeof value === 'number' && Number.isFinite(value) && value >= minimum && value <= maximum
|
||||
? value
|
||||
: undefined;
|
||||
|
||||
function data(response: UpstreamResponse): Record<string, unknown> | undefined {
|
||||
if (response.status < 200 || response.status >= 300) return undefined;
|
||||
if (Buffer.byteLength(response.body, 'utf8') > MAX_BODY_BYTES) return undefined;
|
||||
try {
|
||||
return record(record(JSON.parse(response.body))?.data);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseStats(response: UpstreamResponse): InstanceResources {
|
||||
const value = data(response);
|
||||
if (!value) return {};
|
||||
const cpuPercent = ranged(record(value.cpu_load)?.load_percent, 0, 100);
|
||||
const memoryPercent = ranged(record(value.memory)?.used_percent, 0, 100);
|
||||
const temperatures = Array.isArray(value.temperature)
|
||||
? value.temperature
|
||||
.map((item) => ranged(record(item)?.temperature, -100, 300))
|
||||
.filter((item): item is number => item !== undefined)
|
||||
: [];
|
||||
const maxTemperatureCelsius = temperatures.length > 0 ? Math.max(...temperatures) : undefined;
|
||||
return {
|
||||
...(cpuPercent === undefined ? {} : { cpuPercent }),
|
||||
...(memoryPercent === undefined ? {} : { memoryPercent }),
|
||||
...(maxTemperatureCelsius === undefined ? {} : { maxTemperatureCelsius }),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSim(response: UpstreamResponse): InstanceResources {
|
||||
const value = data(response);
|
||||
if (!value || !Array.isArray(value.phone_numbers)) return {};
|
||||
const phoneNumbers = value.phone_numbers.filter(
|
||||
(item): item is string =>
|
||||
typeof item === 'string' &&
|
||||
item.length > 0 &&
|
||||
item.length <= 32 &&
|
||||
/^[+0-9 ()-]+$/.test(item),
|
||||
);
|
||||
return phoneNumbers.length > 0 ? { phoneNumbers: [...new Set(phoneNumbers)].slice(0, 16) } : {};
|
||||
}
|
||||
|
||||
export class InstanceResourceService {
|
||||
constructor(
|
||||
private readonly options: {
|
||||
readonly instances: InstanceService;
|
||||
readonly sessions: InstanceSessionStore;
|
||||
readonly request: UpstreamSessionClientOptions['request'];
|
||||
},
|
||||
) {}
|
||||
|
||||
async get(instanceId: string): Promise<InstanceResources> {
|
||||
const [instance, session] = await Promise.all([
|
||||
this.options.instances.get(instanceId),
|
||||
Promise.resolve(this.options.sessions.sessionFor(instanceId)),
|
||||
]);
|
||||
if (!instance || !session || session.origin !== instance.origin) return {};
|
||||
const get = (path: '/api/stats' | '/api/sim') =>
|
||||
this.options.request({
|
||||
url: `${instance.origin}${path}`,
|
||||
method: 'GET',
|
||||
headers: { cookie: session.cookie, accept: 'application/json' },
|
||||
});
|
||||
const [stats, sim] = await Promise.allSettled([get('/api/stats'), get('/api/sim')]);
|
||||
return {
|
||||
...(stats.status === 'fulfilled' ? parseStats(stats.value) : {}),
|
||||
...(sim.status === 'fulfilled' ? parseSim(sim.value) : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user