Files
oneuptime/Common/Server/API/TelemetryAPI.ts
Nawaz Dhandala 3c8dc1eee1 feat: Add ProfileTable component for displaying profiling data
- Implemented ProfileTable component to visualize profiles with advanced filtering options.
- Integrated telemetry services and attributes loading for dynamic filtering.
- Added error handling and loading states for improved user experience.

feat: Create ProfileUtil for stack frame parsing and formatting

- Introduced ProfileUtil class with methods for frame type color coding and stack frame parsing.
- Added utility functions for formatting profile values based on units.

docs: Add documentation for telemetry profiles integration

- Created comprehensive guide on sending continuous profiling data to OneUptime.
- Included supported profile types, setup instructions, and instrumentation examples.

feat: Implement ProfileAggregationService for flamegraph and function list retrieval

- Developed ProfileAggregationService to aggregate profile samples and generate flamegraphs.
- Added functionality to retrieve top functions based on various metrics.

feat: Define MonitorStepProfileMonitor interface for profile monitoring

- Created MonitorStepProfileMonitor interface and utility for building queries based on monitoring parameters.

test: Add example OTLP profiles payload for testing

- Included example JSON payload for OTLP profiles to assist in testing and integration.
2026-03-27 11:11:33 +00:00

880 lines
25 KiB
TypeScript

import UserMiddleware from "../Middleware/UserAuthorization";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BadDataException from "../../Types/Exception/BadDataException";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import TelemetryType from "../../Types/Telemetry/TelemetryType";
import TelemetryAttributeService from "../Services/TelemetryAttributeService";
import LogAggregationService, {
HistogramBucket,
HistogramRequest,
FacetValue,
FacetRequest,
AnalyticsRequest,
AnalyticsChartType,
AnalyticsAggregation,
AnalyticsTimeseriesRow,
AnalyticsTopItem,
AnalyticsTableRow,
} from "../Services/LogAggregationService";
import ProfileAggregationService, {
FlamegraphRequest,
FunctionListRequest,
FunctionListItem,
ProfileFlamegraphNode,
} from "../Services/ProfileAggregationService";
import ObjectID from "../../Types/ObjectID";
import OneUptimeDate from "../../Types/Date";
import { JSONObject } from "../../Types/JSON";
const router: ExpressRouter = Express.getRouter();
router.post(
"/telemetry/metrics/get-attributes",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
return getAttributes(req, res, next, TelemetryType.Metric);
},
);
router.post(
"/telemetry/logs/get-attributes",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
return getAttributes(req, res, next, TelemetryType.Log);
},
);
router.post(
"/telemetry/traces/get-attributes",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
return getAttributes(req, res, next, TelemetryType.Trace);
},
);
type GetAttributesFunction = (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
telemetryType: TelemetryType,
) => Promise<void>;
const getAttributes: GetAttributesFunction = async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
telemetryType: TelemetryType,
) => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid User Sesssion"),
);
}
if (!databaseProps.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const attributes: string[] =
await TelemetryAttributeService.fetchAttributes({
projectId: databaseProps.tenantId,
telemetryType,
});
return Response.sendJsonObjectResponse(req, res, {
attributes: attributes,
});
} catch (err: any) {
next(err);
}
};
// --- Log Histogram Endpoint ---
router.post(
"/telemetry/logs/histogram",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const startTime: Date = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
const endTime: Date = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: OneUptimeDate.getCurrentDate();
const bucketSizeInMinutes: number =
(body["bucketSizeInMinutes"] as number) ||
computeDefaultBucketSize(startTime, endTime);
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const severityTexts: Array<string> | undefined = body["severityTexts"]
? (body["severityTexts"] as Array<string>)
: undefined;
const bodySearchText: string | undefined = body["bodySearchText"]
? (body["bodySearchText"] as string)
: undefined;
const traceIds: Array<string> | undefined = body["traceIds"]
? (body["traceIds"] as Array<string>)
: undefined;
const spanIds: Array<string> | undefined = body["spanIds"]
? (body["spanIds"] as Array<string>)
: undefined;
const attributes: Record<string, string> | undefined = body["attributes"]
? (body["attributes"] as Record<string, string>)
: undefined;
const request: HistogramRequest = {
projectId: databaseProps.tenantId,
startTime,
endTime,
bucketSizeInMinutes,
serviceIds,
severityTexts,
bodySearchText,
traceIds,
spanIds,
attributes,
};
const buckets: Array<HistogramBucket> =
await LogAggregationService.getHistogram(request);
return Response.sendJsonObjectResponse(req, res, {
buckets: buckets as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
// --- Log Facets Endpoint ---
router.post(
"/telemetry/logs/facets",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const facetKeys: Array<string> = body["facetKeys"]
? (body["facetKeys"] as Array<string>)
: ["severityText", "serviceId"];
const startTime: Date = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
const endTime: Date = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: OneUptimeDate.getCurrentDate();
const limit: number = (body["limit"] as number) || 500;
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const severityTexts: Array<string> | undefined = body["severityTexts"]
? (body["severityTexts"] as Array<string>)
: undefined;
const bodySearchText: string | undefined = body["bodySearchText"]
? (body["bodySearchText"] as string)
: undefined;
const traceIds: Array<string> | undefined = body["traceIds"]
? (body["traceIds"] as Array<string>)
: undefined;
const spanIds: Array<string> | undefined = body["spanIds"]
? (body["spanIds"] as Array<string>)
: undefined;
const attributes: Record<string, string> | undefined = body["attributes"]
? (body["attributes"] as Record<string, string>)
: undefined;
const facets: Record<string, Array<FacetValue>> = {};
for (const facetKey of facetKeys) {
const request: FacetRequest = {
projectId: databaseProps.tenantId,
startTime,
endTime,
facetKey,
limit,
serviceIds,
severityTexts,
bodySearchText,
traceIds,
spanIds,
attributes,
};
facets[facetKey] = await LogAggregationService.getFacetValues(request);
}
return Response.sendJsonObjectResponse(req, res, {
facets: facets as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
// --- Log Analytics Endpoint ---
router.post(
"/telemetry/logs/analytics",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const chartType: AnalyticsChartType =
(body["chartType"] as AnalyticsChartType) || "timeseries";
if (!["timeseries", "toplist", "table"].includes(chartType)) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid chartType"),
);
}
const aggregation: AnalyticsAggregation =
(body["aggregation"] as AnalyticsAggregation) || "count";
if (!["count", "unique"].includes(aggregation)) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid aggregation"),
);
}
const startTime: Date = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
const endTime: Date = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: OneUptimeDate.getCurrentDate();
const bucketSizeInMinutes: number =
(body["bucketSizeInMinutes"] as number) ||
computeDefaultBucketSize(startTime, endTime);
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const severityTexts: Array<string> | undefined = body["severityTexts"]
? (body["severityTexts"] as Array<string>)
: undefined;
const bodySearchText: string | undefined = body["bodySearchText"]
? (body["bodySearchText"] as string)
: undefined;
const traceIds: Array<string> | undefined = body["traceIds"]
? (body["traceIds"] as Array<string>)
: undefined;
const spanIds: Array<string> | undefined = body["spanIds"]
? (body["spanIds"] as Array<string>)
: undefined;
const groupBy: Array<string> | undefined = body["groupBy"]
? (body["groupBy"] as Array<string>)
: undefined;
const aggregationField: string | undefined = body["aggregationField"]
? (body["aggregationField"] as string)
: undefined;
const limit: number | undefined = body["limit"]
? (body["limit"] as number)
: undefined;
const request: AnalyticsRequest = {
projectId: databaseProps.tenantId,
startTime,
endTime,
bucketSizeInMinutes,
chartType,
groupBy,
aggregation,
aggregationField,
serviceIds,
severityTexts,
bodySearchText,
traceIds,
spanIds,
limit,
};
if (chartType === "timeseries") {
const data: Array<AnalyticsTimeseriesRow> =
await LogAggregationService.getAnalyticsTimeseries(request);
return Response.sendJsonObjectResponse(req, res, {
data: data as unknown as JSONObject,
});
}
if (chartType === "toplist") {
const data: Array<AnalyticsTopItem> =
await LogAggregationService.getAnalyticsTopList(request);
return Response.sendJsonObjectResponse(req, res, {
data: data as unknown as JSONObject,
});
}
// table
const data: Array<AnalyticsTableRow> =
await LogAggregationService.getAnalyticsTable(request);
return Response.sendJsonObjectResponse(req, res, {
data: data as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
// --- Log Export Endpoint ---
router.post(
"/telemetry/logs/export",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const startTime: Date = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
const endTime: Date = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: OneUptimeDate.getCurrentDate();
const limit: number = Math.min((body["limit"] as number) || 10000, 10000);
const format: string = (body["format"] as string) || "json";
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const severityTexts: Array<string> | undefined = body["severityTexts"]
? (body["severityTexts"] as Array<string>)
: undefined;
const bodySearchText: string | undefined = body["bodySearchText"]
? (body["bodySearchText"] as string)
: undefined;
const traceIds: Array<string> | undefined = body["traceIds"]
? (body["traceIds"] as Array<string>)
: undefined;
const spanIds: Array<string> | undefined = body["spanIds"]
? (body["spanIds"] as Array<string>)
: undefined;
const rows: Array<JSONObject> = await LogAggregationService.getExportLogs(
{
projectId: databaseProps.tenantId,
startTime,
endTime,
limit,
serviceIds,
severityTexts,
bodySearchText,
traceIds,
spanIds,
},
);
if (format === "csv") {
const header: string =
"time,serviceId,severityText,severityNumber,body,traceId,spanId,attributes";
const csvRows: Array<string> = rows.map((row: JSONObject) => {
const escapeCsv: (val: unknown) => string = (
val: unknown,
): string => {
const str: string =
val === null || val === undefined ? "" : String(val);
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
return [
escapeCsv(row["time"]),
escapeCsv(row["serviceId"]),
escapeCsv(row["severityText"]),
escapeCsv(row["severityNumber"]),
escapeCsv(row["body"]),
escapeCsv(row["traceId"]),
escapeCsv(row["spanId"]),
escapeCsv(JSON.stringify(row["attributes"] || {})),
].join(",");
});
const csv: string = [header, ...csvRows].join("\n");
res.setHeader("Content-Type", "text/csv; charset=utf-8");
res.setHeader(
"Content-Disposition",
"attachment; filename=logs-export.csv",
);
res.status(200).send(csv);
return;
}
// JSON format
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.setHeader(
"Content-Disposition",
"attachment; filename=logs-export.json",
);
res.status(200).send(JSON.stringify(rows, null, 2));
} catch (err: unknown) {
next(err);
}
},
);
// --- Log Context Endpoint ---
router.post(
"/telemetry/logs/context",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const logId: string | undefined = body["logId"] as string | undefined;
const serviceId: string | undefined = body["serviceId"] as
| string
| undefined;
const time: string | undefined = body["time"] as string | undefined;
if (!logId || !serviceId || !time) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("logId, serviceId, and time are required"),
);
}
const count: number = (body["count"] as number) || 5;
const result: {
before: Array<JSONObject>;
after: Array<JSONObject>;
} = await LogAggregationService.getLogContext({
projectId: databaseProps.tenantId,
serviceId: new ObjectID(serviceId),
time: OneUptimeDate.fromString(time),
logId,
count,
});
return Response.sendJsonObjectResponse(req, res, {
before: result.before as unknown as JSONObject,
after: result.after as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
// --- Drop Filter Estimate Endpoint ---
router.post(
"/telemetry/logs/drop-filter-estimate",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const filterQuery: string | undefined = body["filterQuery"] as
| string
| undefined;
if (!filterQuery) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("filterQuery is required"),
);
}
const startTime: Date = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -24);
const endTime: Date = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: OneUptimeDate.getCurrentDate();
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const severityTexts: Array<string> | undefined = body["severityTexts"]
? (body["severityTexts"] as Array<string>)
: undefined;
const result: {
totalLogs: number;
matchingLogs: number;
estimatedReductionPercent: number;
} = await LogAggregationService.getDropFilterEstimate({
projectId: databaseProps.tenantId,
startTime,
endTime,
filterQuery,
serviceIds,
severityTexts,
});
return Response.sendJsonObjectResponse(req, res, {
totalLogs: result.totalLogs,
matchingLogs: result.matchingLogs,
estimatedReductionPercent: result.estimatedReductionPercent,
} as unknown as JSONObject);
} catch (err: unknown) {
next(err);
}
},
);
// --- Helpers ---
function computeDefaultBucketSize(startTime: Date, endTime: Date): number {
const diffMs: number = endTime.getTime() - startTime.getTime();
const diffMinutes: number = diffMs / (1000 * 60);
if (diffMinutes <= 60) {
return 1;
}
if (diffMinutes <= 360) {
return 5;
}
if (diffMinutes <= 1440) {
return 15;
}
if (diffMinutes <= 10080) {
return 60;
}
if (diffMinutes <= 43200) {
return 360;
}
return 1440;
}
// --- Profile Get Attributes Endpoint ---
router.post(
"/telemetry/profiles/get-attributes",
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
return getAttributes(req, res, next, TelemetryType.Profile);
},
);
// --- Profile Flamegraph Endpoint ---
router.post(
"/telemetry/profiles/flamegraph",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const profileId: string | undefined = body["profileId"]
? (body["profileId"] as string)
: undefined;
const startTime: Date | undefined = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: undefined;
const endTime: Date | undefined = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: undefined;
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const profileType: string | undefined = body["profileType"]
? (body["profileType"] as string)
: undefined;
if (!profileId && !startTime) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"Either profileId or startTime must be provided",
),
);
}
const request: FlamegraphRequest = {
projectId: databaseProps.tenantId,
profileId,
startTime,
endTime,
serviceIds,
profileType,
};
const flamegraph: ProfileFlamegraphNode =
await ProfileAggregationService.getFlamegraph(request);
return Response.sendJsonObjectResponse(req, res, {
flamegraph: flamegraph as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
// --- Profile Function List Endpoint ---
router.post(
"/telemetry/profiles/function-list",
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
const databaseProps: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
if (!databaseProps?.tenantId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Invalid Project ID"),
);
}
const body: JSONObject = req.body as JSONObject;
const startTime: Date = body["startTime"]
? OneUptimeDate.fromString(body["startTime"] as string)
: OneUptimeDate.addRemoveHours(OneUptimeDate.getCurrentDate(), -1);
const endTime: Date = body["endTime"]
? OneUptimeDate.fromString(body["endTime"] as string)
: OneUptimeDate.getCurrentDate();
const serviceIds: Array<ObjectID> | undefined = body["serviceIds"]
? (body["serviceIds"] as Array<string>).map((id: string) => {
return new ObjectID(id);
})
: undefined;
const profileType: string | undefined = body["profileType"]
? (body["profileType"] as string)
: undefined;
const limit: number | undefined = body["limit"]
? (body["limit"] as number)
: undefined;
const sortBy: "selfValue" | "totalValue" | "sampleCount" | undefined =
body["sortBy"]
? (body["sortBy"] as "selfValue" | "totalValue" | "sampleCount")
: undefined;
const request: FunctionListRequest = {
projectId: databaseProps.tenantId,
startTime,
endTime,
serviceIds,
profileType,
limit,
sortBy,
};
const functions: Array<FunctionListItem> =
await ProfileAggregationService.getFunctionList(request);
return Response.sendJsonObjectResponse(req, res, {
functions: functions as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
export default router;