Files
oneuptime/Common/Server/API/TelemetryAPI.ts

1107 lines
31 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,
DiffFlamegraphRequest,
DiffFlamegraphNode,
} from "../Services/ProfileAggregationService";
import PprofEncoder, {
PprofProfile,
PprofSample,
} from "../Utils/Profile/PprofEncoder";
import Profile from "../../Models/AnalyticsModels/Profile";
import ProfileSample from "../../Models/AnalyticsModels/ProfileSample";
import ProfileService from "../Services/ProfileService";
import ProfileSampleService from "../Services/ProfileSampleService";
import SortOrder from "../../Types/BaseDatabase/SortOrder";
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 !== undefined && { profileId }),
...(startTime !== undefined && { startTime }),
...(endTime !== undefined && { endTime }),
...(serviceIds !== undefined && { serviceIds }),
...(profileType !== undefined && { 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 !== undefined && { serviceIds }),
...(profileType !== undefined && { profileType }),
...(limit !== undefined && { limit }),
...(sortBy !== undefined && { 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);
}
},
);
// --- Profile pprof Export Endpoint ---
router.get(
"/telemetry/profiles/:profileId/pprof",
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 profileId: string | undefined = req.params["profileId"];
if (!profileId) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("profileId is required"),
);
}
// Fetch profile metadata
const profiles: Array<Profile> = await ProfileService.findBy({
query: {
projectId: databaseProps.tenantId,
profileId: profileId,
},
select: {
profileId: true,
profileType: true,
unit: true,
periodType: true,
period: true,
startTime: true,
endTime: true,
durationNano: true,
},
limit: 1,
skip: 0,
props: {
isRoot: true,
},
});
if (!profiles[0]) {
return Response.sendErrorResponse(
req,
res,
new BadDataException("Profile not found"),
);
}
const profile: Profile = profiles[0];
// Fetch profile samples
const samplesResult: Array<ProfileSample> =
await ProfileSampleService.findBy({
query: {
projectId: databaseProps.tenantId,
profileId: profileId,
},
select: {
stacktrace: true,
value: true,
labels: true,
},
limit: 50000,
skip: 0,
sort: {
value: SortOrder.Descending,
},
props: {
isRoot: true,
},
});
const pprofSamples: Array<PprofSample> = samplesResult.map(
(sample: ProfileSample): PprofSample => {
return {
stacktrace: sample.stacktrace || [],
value: sample.value || 0,
labels: sample.labels as JSONObject | undefined,
};
},
);
const pprofProfile: PprofProfile = {
profileId: profile.profileId || profileId,
profileType: profile.profileType || "cpu",
unit: profile.unit || "nanoseconds",
periodType: profile.periodType || "cpu",
period: profile.period || 0,
startTimeNanos: profile.startTime
? new Date(profile.startTime).getTime() * 1000000
: 0,
endTimeNanos: profile.endTime
? new Date(profile.endTime).getTime() * 1000000
: 0,
durationNanos: profile.durationNano || 0,
samples: pprofSamples,
};
const compressed: Buffer =
await PprofEncoder.encodeAndCompress(pprofProfile);
res.setHeader("Content-Type", "application/x-protobuf");
res.setHeader(
"Content-Disposition",
`attachment; filename=profile-${profileId}.pb.gz`,
);
res.setHeader("Content-Length", compressed.length.toString());
res.send(compressed);
} catch (err: unknown) {
next(err);
}
},
);
// --- Profile Diff Flamegraph Endpoint ---
router.post(
"/telemetry/profiles/diff-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 baselineStartTime: Date | undefined = body["baselineStartTime"]
? OneUptimeDate.fromString(body["baselineStartTime"] as string)
: undefined;
const baselineEndTime: Date | undefined = body["baselineEndTime"]
? OneUptimeDate.fromString(body["baselineEndTime"] as string)
: undefined;
const comparisonStartTime: Date | undefined = body["comparisonStartTime"]
? OneUptimeDate.fromString(body["comparisonStartTime"] as string)
: undefined;
const comparisonEndTime: Date | undefined = body["comparisonEndTime"]
? OneUptimeDate.fromString(body["comparisonEndTime"] as string)
: undefined;
if (
!baselineStartTime ||
!baselineEndTime ||
!comparisonStartTime ||
!comparisonEndTime
) {
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"baselineStartTime, baselineEndTime, comparisonStartTime, and comparisonEndTime are all required",
),
);
}
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 request: DiffFlamegraphRequest = {
projectId: databaseProps.tenantId,
baselineStartTime,
baselineEndTime,
comparisonStartTime,
comparisonEndTime,
...(serviceIds !== undefined && { serviceIds }),
...(profileType !== undefined && { profileType }),
};
const diffFlamegraph: DiffFlamegraphNode =
await ProfileAggregationService.getDiffFlamegraph(request);
return Response.sendJsonObjectResponse(req, res, {
diffFlamegraph: diffFlamegraph as unknown as JSONObject,
});
} catch (err: unknown) {
next(err);
}
},
);
export default router;