234 lines
5.6 KiB
TypeScript
234 lines
5.6 KiB
TypeScript
import { type ClickHouse } from "@internal/clickhouse";
|
|
import type { TaskRunStatus } from "@trigger.dev/database";
|
|
import { QUEUED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
|
|
|
export type DailyTaskActivity = Record<string, ({ day: string } & Record<TaskRunStatus, number>)[]>;
|
|
export type CurrentRunningStats = Record<string, { queued: number; running: number }>;
|
|
export type AverageDurations = Record<string, number>;
|
|
|
|
export interface EnvironmentMetricsRepository {
|
|
getDailyTaskActivity(options: {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
days: number;
|
|
tasks: string[];
|
|
}): Promise<DailyTaskActivity>;
|
|
|
|
getCurrentRunningStats(options: {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
days: number;
|
|
tasks: string[];
|
|
}): Promise<CurrentRunningStats>;
|
|
|
|
getAverageDurations(options: {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
days: number;
|
|
tasks: string[];
|
|
}): Promise<AverageDurations>;
|
|
}
|
|
|
|
export type ClickHouseEnvironmentMetricsRepositoryOptions = {
|
|
clickhouse: ClickHouse;
|
|
};
|
|
|
|
export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetricsRepository {
|
|
constructor(private readonly options: ClickHouseEnvironmentMetricsRepositoryOptions) {}
|
|
|
|
public async getDailyTaskActivity({
|
|
organizationId,
|
|
projectId,
|
|
environmentId,
|
|
days,
|
|
tasks,
|
|
}: {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
days: number;
|
|
tasks: string[];
|
|
}): Promise<DailyTaskActivity> {
|
|
if (tasks.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
const [queryError, activity] = await this.options.clickhouse.taskRuns.getTaskActivity({
|
|
organizationId,
|
|
projectId,
|
|
environmentId,
|
|
days,
|
|
});
|
|
|
|
if (queryError) {
|
|
throw queryError;
|
|
}
|
|
|
|
return fillInDailyTaskActivity(
|
|
activity.map((a) => ({
|
|
taskIdentifier: a.task_identifier,
|
|
status: a.status as TaskRunStatus,
|
|
day: new Date(a.day),
|
|
count: BigInt(a.count),
|
|
})),
|
|
days
|
|
);
|
|
}
|
|
|
|
public async getCurrentRunningStats({
|
|
organizationId,
|
|
projectId,
|
|
environmentId,
|
|
days,
|
|
tasks,
|
|
}: {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
days: number;
|
|
tasks: string[];
|
|
}): Promise<CurrentRunningStats> {
|
|
if (tasks.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
const [queryError, stats] = await this.options.clickhouse.taskRuns.getCurrentRunningStats({
|
|
organizationId,
|
|
projectId,
|
|
environmentId,
|
|
days,
|
|
});
|
|
|
|
if (queryError) {
|
|
throw queryError;
|
|
}
|
|
|
|
return fillInCurrentRunningStats(
|
|
stats.map((s) => ({
|
|
taskIdentifier: s.task_identifier,
|
|
status: s.status as TaskRunStatus,
|
|
count: BigInt(s.count),
|
|
})),
|
|
tasks
|
|
);
|
|
}
|
|
|
|
public async getAverageDurations({
|
|
organizationId,
|
|
projectId,
|
|
environmentId,
|
|
days,
|
|
tasks,
|
|
}: {
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
days: number;
|
|
tasks: string[];
|
|
}): Promise<AverageDurations> {
|
|
if (tasks.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
const [queryError, durations] = await this.options.clickhouse.taskRuns.getAverageDurations({
|
|
organizationId,
|
|
projectId,
|
|
environmentId,
|
|
days,
|
|
});
|
|
|
|
if (queryError) {
|
|
throw queryError;
|
|
}
|
|
|
|
return Object.fromEntries(durations.map((d) => [d.task_identifier, Number(d.duration)]));
|
|
}
|
|
}
|
|
|
|
type TaskActivityResults = Array<{
|
|
taskIdentifier: string;
|
|
status: TaskRunStatus;
|
|
day: Date;
|
|
count: bigint;
|
|
}>;
|
|
|
|
function fillInDailyTaskActivity(activity: TaskActivityResults, days: number): DailyTaskActivity {
|
|
//today with no time
|
|
const today = new Date();
|
|
today.setUTCHours(0, 0, 0, 0);
|
|
|
|
return activity.reduce((acc, a) => {
|
|
let existingTask = acc[a.taskIdentifier];
|
|
|
|
if (!existingTask) {
|
|
existingTask = [];
|
|
//populate the array with the past 7 days
|
|
for (let i = days; i >= 0; i--) {
|
|
const day = new Date(today);
|
|
day.setUTCDate(today.getDate() - i);
|
|
day.setUTCHours(0, 0, 0, 0);
|
|
|
|
existingTask.push({
|
|
day: day.toISOString(),
|
|
["COMPLETED_SUCCESSFULLY"]: 0,
|
|
} as { day: string } & Record<TaskRunStatus, number>);
|
|
}
|
|
|
|
acc[a.taskIdentifier] = existingTask;
|
|
}
|
|
|
|
const dayString = a.day.toISOString();
|
|
const day = existingTask.find((d) => d.day === dayString);
|
|
|
|
if (!day) {
|
|
return acc;
|
|
}
|
|
|
|
day[a.status] = Number(a.count);
|
|
|
|
return acc;
|
|
}, {} as DailyTaskActivity);
|
|
}
|
|
|
|
type CurrentRunningStatsResults = Array<{
|
|
taskIdentifier: string;
|
|
status: TaskRunStatus;
|
|
count: bigint;
|
|
}>;
|
|
|
|
function fillInCurrentRunningStats(
|
|
stats: CurrentRunningStatsResults,
|
|
tasks: string[]
|
|
): CurrentRunningStats {
|
|
//create an object combining the queued and concurrency counts
|
|
const result: Record<string, { queued: number; running: number }> = {};
|
|
for (const task of tasks) {
|
|
const queued = stats.filter(
|
|
(q) => q.taskIdentifier === task && QUEUED_STATUSES.includes(q.status)
|
|
);
|
|
const queuedCount =
|
|
queued.length === 0
|
|
? 0
|
|
: queued.reduce((acc, q) => {
|
|
return acc + Number(q.count);
|
|
}, 0);
|
|
|
|
const running = stats.filter((r) => r.taskIdentifier === task && r.status === "EXECUTING");
|
|
const runningCount =
|
|
running.length === 0
|
|
? 0
|
|
: running.reduce((acc, r) => {
|
|
return acc + Number(r.count);
|
|
}, 0);
|
|
|
|
result[task] = {
|
|
queued: queuedCount,
|
|
running: runningCount,
|
|
};
|
|
}
|
|
return result;
|
|
}
|