import * as plugins from './plugins.js'; import type { IControllerSystemMetrics, IControllerSystemMetricsHistoryPoint, } from '../ts_interfaces/index.js'; export const systemMetricsHistoryWindowMs = 60 * 60 * 1_000; export const systemMetricsHistoryResolutionMs = 10_000; const systemMetricsHistoryRetentionMs = systemMetricsHistoryWindowMs + 5 * 60 * 1_000; const maximumHistoryPoints = 400; interface IControllerSystemMetricsHistoryDocument { id: string; sampledAt: number; expiresAt: Date; point: IControllerSystemMetricsHistoryPoint; } @plugins.smartdata.managed({ collectionName: 'opencode_controller_system_metrics_history' }) export class ControllerSystemMetricsHistoryModel extends plugins.smartdata.SmartDataDbDoc< ControllerSystemMetricsHistoryModel, IControllerSystemMetricsHistoryDocument > { @plugins.smartdata.unI() public id!: string; @plugins.smartdata.index() @plugins.smartdata.svDb() public sampledAt!: number; @plugins.smartdata.index({ expireAfterSeconds: 0 }) @plugins.smartdata.svDb() public expiresAt!: Date; @plugins.smartdata.svDb() public point!: IControllerSystemMetricsHistoryPoint; } export const toSystemMetricsHistoryPoint = ( metricsArg: IControllerSystemMetrics, ): IControllerSystemMetricsHistoryPoint => ({ sampledAt: metricsArg.sampledAt, cpuUsagePercent: metricsArg.cpuUsagePercent, memoryUsedPercent: metricsArg.memoryUsedBytes !== null && metricsArg.memoryTotalBytes !== null && metricsArg.memoryTotalBytes > 0 ? (metricsArg.memoryUsedBytes / metricsArg.memoryTotalBytes) * 100 : null, networkReceiveBytesPerSecond: metricsArg.networkReceiveBytesPerSecond, networkTransmitBytesPerSecond: metricsArg.networkTransmitBytesPerSecond, disks: [metricsArg.mainDisk, ...metricsArg.volumes] .filter((diskArg): diskArg is NonNullable => diskArg !== null) .map((diskArg) => ({ mountPoint: diskArg.mountPoint, usedPercent: diskArg.usedPercent, busyPercent: diskArg.io?.busyPercent ?? null, })) .sort((leftArg, rightArg) => leftArg.mountPoint.localeCompare(rightArg.mountPoint)), }); export const recordSystemMetricsHistoryPoint = async ( metricsArg: IControllerSystemMetrics, ): Promise => { const point = toSystemMetricsHistoryPoint(metricsArg); const bucket = Math.floor(point.sampledAt / systemMetricsHistoryResolutionMs); const id = String(bucket); const stored = await ControllerSystemMetricsHistoryModel.getInstance({ id }); const document = stored ?? new ControllerSystemMetricsHistoryModel(); document.id = id; document.sampledAt = point.sampledAt; document.expiresAt = new Date(point.sampledAt + systemMetricsHistoryRetentionMs); document.point = point; await document.save(); }; export const listSystemMetricsHistoryPoints = async ( nowArg: number = Date.now(), ): Promise => { const cursor = await ControllerSystemMetricsHistoryModel.getCursor( { sampledAt: { $gte: nowArg - systemMetricsHistoryWindowMs, $lte: nowArg } }, { sort: { sampledAt: -1, id: -1 }, limit: maximumHistoryPoints }, ); const documents = await cursor.toArray(); return documents.reverse().map((documentArg) => documentArg.point); }; /** TTL is an additional guard; explicit bounded deletion also works on embedded SmartDB. */ export const pruneSystemMetricsHistory = async (nowArg: number = Date.now()): Promise => { const cursor = await ControllerSystemMetricsHistoryModel.getCursor( { sampledAt: { $lt: nowArg - systemMetricsHistoryWindowMs } }, { sort: { sampledAt: 1, id: 1 }, limit: maximumHistoryPoints }, ); const expired = await cursor.toArray(); for (const document of expired) await document.delete(); };