mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: implement captured metrics handling in custom code and synthetic monitors
This commit is contained in:
@@ -13,6 +13,7 @@ import MetricType from "../../../Models/DatabaseModels/MetricType";
|
||||
import BasicInfrastructureMetrics from "../../../Types/Infrastructure/BasicMetrics";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
import { JSONObject } from "../../../Types/JSON";
|
||||
import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
|
||||
import MonitorMetricType from "../../../Types/Monitor/MonitorMetricType";
|
||||
import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
|
||||
import ServerMonitorResponse from "../../../Types/Monitor/ServerMonitor/ServerMonitorResponse";
|
||||
@@ -533,6 +534,91 @@ export default class MonitorMetricUtil {
|
||||
metricType;
|
||||
}
|
||||
|
||||
// Process custom metrics from Custom Code and Synthetic Monitor responses
|
||||
const customCodeMetrics: CapturedMetric[] =
|
||||
(data.dataToProcess as ProbeMonitorResponse).customCodeMonitorResponse
|
||||
?.capturedMetrics || [];
|
||||
|
||||
const syntheticCustomMetrics: CapturedMetric[] = [];
|
||||
const syntheticResponsesForMetrics: Array<SyntheticMonitorResponse> =
|
||||
(data.dataToProcess as ProbeMonitorResponse).syntheticMonitorResponse ||
|
||||
[];
|
||||
for (const resp of syntheticResponsesForMetrics) {
|
||||
if (resp.capturedMetrics) {
|
||||
syntheticCustomMetrics.push(...resp.capturedMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
const allCustomMetrics: CapturedMetric[] = [
|
||||
...customCodeMetrics,
|
||||
...syntheticCustomMetrics,
|
||||
].slice(0, 100);
|
||||
|
||||
const reservedAttributeKeys: Set<string> = new Set([
|
||||
"monitorId",
|
||||
"projectId",
|
||||
"monitorName",
|
||||
"probeName",
|
||||
"probeId",
|
||||
]);
|
||||
|
||||
for (const customMetric of allCustomMetrics) {
|
||||
if (
|
||||
!customMetric.name ||
|
||||
typeof customMetric.name !== "string" ||
|
||||
typeof customMetric.value !== "number" ||
|
||||
isNaN(customMetric.value)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const prefixedName: string = `custom.monitor.${customMetric.name}`;
|
||||
|
||||
const extraAttributes: JSONObject = {
|
||||
isCustomMetric: "true",
|
||||
};
|
||||
|
||||
if ((data.dataToProcess as ProbeMonitorResponse).probeId) {
|
||||
extraAttributes["probeId"] = (
|
||||
data.dataToProcess as ProbeMonitorResponse
|
||||
).probeId.toString();
|
||||
}
|
||||
|
||||
if (customMetric.attributes) {
|
||||
for (const [key, val] of Object.entries(customMetric.attributes)) {
|
||||
if (typeof val === "string" && !reservedAttributeKeys.has(key)) {
|
||||
extraAttributes[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const attributes: JSONObject = this.buildMonitorMetricAttributes({
|
||||
monitorId: data.monitorId,
|
||||
projectId: data.projectId,
|
||||
monitorName: data.monitorName,
|
||||
probeName: data.probeName,
|
||||
extraAttributes: extraAttributes,
|
||||
});
|
||||
|
||||
const metricRow: JSONObject = await this.buildMonitorMetricRow({
|
||||
projectId: data.projectId,
|
||||
monitorId: data.monitorId,
|
||||
metricName: prefixedName,
|
||||
value: customMetric.value,
|
||||
attributes: attributes,
|
||||
metricPointType: MetricPointType.Gauge,
|
||||
});
|
||||
|
||||
metricRows.push(metricRow);
|
||||
|
||||
const metricType: MetricType = new MetricType();
|
||||
metricType.name = prefixedName;
|
||||
metricType.description = `Custom metric: ${customMetric.name}`;
|
||||
metricType.unit = "";
|
||||
|
||||
metricNameServiceNameMap[prefixedName] = metricType;
|
||||
}
|
||||
|
||||
if (metricRows.length > 0) {
|
||||
await MetricService.insertJsonRows(metricRows);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
|
||||
import ReturnResult from "../../../Types/IsolatedVM/ReturnResult";
|
||||
import { JSONObject, JSONValue } from "../../../Types/JSON";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
@@ -310,6 +311,9 @@ export default class VMRunner {
|
||||
const MAX_LOG_BYTES: number = 1_000_000; // 1MB cap
|
||||
let totalLogBytes: number = 0;
|
||||
|
||||
const capturedMetrics: CapturedMetric[] = [];
|
||||
const MAX_METRICS: number = 100;
|
||||
|
||||
// Track timer handles so we can clean them up after execution
|
||||
type TimerHandle = ReturnType<typeof setTimeout>;
|
||||
const pendingTimeouts: TimerHandle[] = [];
|
||||
@@ -398,6 +402,47 @@ export default class VMRunner {
|
||||
proxyCache,
|
||||
);
|
||||
|
||||
sandbox["oneuptime"] = createSandboxProxy(
|
||||
{
|
||||
captureMetric: (
|
||||
name: unknown,
|
||||
value: unknown,
|
||||
attributes?: unknown,
|
||||
): void => {
|
||||
if (typeof name !== "string" || name.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "number" || isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
if (capturedMetrics.length >= MAX_METRICS) {
|
||||
return;
|
||||
}
|
||||
const metric: CapturedMetric = {
|
||||
name: name.substring(0, 200),
|
||||
value: value,
|
||||
};
|
||||
if (attributes && typeof attributes === "object") {
|
||||
const safeAttrs: JSONObject = {};
|
||||
for (const [k, v] of Object.entries(
|
||||
attributes as Record<string, unknown>,
|
||||
)) {
|
||||
if (
|
||||
typeof v === "string" ||
|
||||
typeof v === "number" ||
|
||||
typeof v === "boolean"
|
||||
) {
|
||||
safeAttrs[k] = String(v);
|
||||
}
|
||||
}
|
||||
metric.attributes = safeAttrs;
|
||||
}
|
||||
capturedMetrics.push(metric);
|
||||
},
|
||||
},
|
||||
proxyCache,
|
||||
);
|
||||
|
||||
// Wrap any additional context (e.g. Playwright browser/page objects)
|
||||
if (options.context) {
|
||||
for (const key of Object.keys(options.context)) {
|
||||
@@ -450,6 +495,7 @@ export default class VMRunner {
|
||||
return {
|
||||
returnValue: deepUnwrapProxies(returnVal),
|
||||
logMessages,
|
||||
capturedMetrics,
|
||||
};
|
||||
} finally {
|
||||
// Clean up any lingering timers to prevent resource leaks
|
||||
@@ -474,6 +520,8 @@ export default class VMRunner {
|
||||
const timeout: number = options.timeout || 5000;
|
||||
|
||||
const logMessages: string[] = [];
|
||||
const capturedMetrics: CapturedMetric[] = [];
|
||||
const MAX_METRICS: number = 100;
|
||||
|
||||
const isolate: ivm.Isolate = new ivm.Isolate({ memoryLimit: 128 });
|
||||
|
||||
@@ -499,6 +547,45 @@ export default class VMRunner {
|
||||
}))};
|
||||
`);
|
||||
|
||||
// oneuptime.captureMetric - fire-and-forget callback
|
||||
await jail.set(
|
||||
"_captureMetric",
|
||||
new ivm.Callback(
|
||||
(name: string, value: string, attributesJson?: string) => {
|
||||
if (capturedMetrics.length >= MAX_METRICS) {
|
||||
return;
|
||||
}
|
||||
const numValue: number = Number(value);
|
||||
if (isNaN(numValue)) {
|
||||
return;
|
||||
}
|
||||
const metric: CapturedMetric = {
|
||||
name: String(name).substring(0, 200),
|
||||
value: numValue,
|
||||
};
|
||||
if (attributesJson) {
|
||||
try {
|
||||
metric.attributes = JSON.parse(attributesJson) as JSONObject;
|
||||
} catch {
|
||||
// ignore invalid JSON
|
||||
}
|
||||
}
|
||||
capturedMetrics.push(metric);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await context.eval(`
|
||||
const oneuptime = {
|
||||
captureMetric: (name, value, attributes) => {
|
||||
if (typeof name !== 'string' || name.length === 0) return;
|
||||
if (typeof value !== 'number' || isNaN(value)) return;
|
||||
const attrJson = attributes ? JSON.stringify(attributes) : undefined;
|
||||
_captureMetric(String(name), String(value), attrJson);
|
||||
}
|
||||
};
|
||||
`);
|
||||
|
||||
// args - deep copy into isolate
|
||||
if (options.args) {
|
||||
await jail.set("_args", new ivm.ExternalCopy(options.args).copyInto());
|
||||
@@ -961,6 +1048,7 @@ export default class VMRunner {
|
||||
return {
|
||||
returnValue,
|
||||
logMessages,
|
||||
capturedMetrics,
|
||||
};
|
||||
} finally {
|
||||
if (!isolate.isDisposed) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import CapturedMetric from "../Monitor/CustomCodeMonitor/CapturedMetric";
|
||||
|
||||
export default interface ReturnResult {
|
||||
returnValue: any;
|
||||
logMessages: string[];
|
||||
capturedMetrics: CapturedMetric[];
|
||||
}
|
||||
|
||||
7
Common/Types/Monitor/CustomCodeMonitor/CapturedMetric.ts
Normal file
7
Common/Types/Monitor/CustomCodeMonitor/CapturedMetric.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { JSONObject } from "../../JSON";
|
||||
|
||||
export default interface CapturedMetric {
|
||||
name: string;
|
||||
value: number;
|
||||
attributes?: JSONObject | undefined;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import CapturedMetric from "./CapturedMetric";
|
||||
import { JSONObject } from "../../JSON";
|
||||
|
||||
export default interface CustomCodeMonitorResponse {
|
||||
result: string | number | boolean | JSONObject | undefined;
|
||||
scriptError?: string | undefined;
|
||||
logMessages: string[];
|
||||
capturedMetrics: CapturedMetric[];
|
||||
executionTimeInMS: number;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class CustomCodeMonitor {
|
||||
|
||||
const scriptResult: CustomCodeMonitorResponse = {
|
||||
logMessages: [],
|
||||
capturedMetrics: [],
|
||||
scriptError: undefined,
|
||||
result: undefined,
|
||||
|
||||
@@ -60,6 +61,7 @@ export default class CustomCodeMonitor {
|
||||
scriptResult.executionTimeInMS = executionTimeInMS;
|
||||
|
||||
scriptResult.logMessages = result.logMessages;
|
||||
scriptResult.capturedMetrics = result.capturedMetrics || [];
|
||||
|
||||
scriptResult.result = result?.returnValue?.data;
|
||||
} catch (err) {
|
||||
|
||||
@@ -127,6 +127,7 @@ export default class SyntheticMonitor {
|
||||
|
||||
const scriptResult: SyntheticMonitorResponse = {
|
||||
logMessages: [],
|
||||
capturedMetrics: [],
|
||||
scriptError: undefined,
|
||||
result: undefined,
|
||||
screenshots: {},
|
||||
@@ -179,6 +180,7 @@ export default class SyntheticMonitor {
|
||||
scriptResult.executionTimeInMS = executionTimeInMS;
|
||||
|
||||
scriptResult.logMessages = result.logMessages;
|
||||
scriptResult.capturedMetrics = result.capturedMetrics || [];
|
||||
|
||||
if (result.returnValue?.screenshots) {
|
||||
if (!scriptResult.screenshots) {
|
||||
|
||||
Reference in New Issue
Block a user