mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(core): Workflow Execution Statistics (#4200)
Add recording and reporting of workflow execution statistics
This commit is contained in:
185
packages/cli/src/api/workflowStats.api.ts
Normal file
185
packages/cli/src/api/workflowStats.api.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { User } from '@/databases/entities/User';
|
||||
import { whereClause } from '@/UserManagement/UserManagementHelper';
|
||||
import express from 'express';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import {
|
||||
Db,
|
||||
IWorkflowStatisticsCounts,
|
||||
IWorkflowStatisticsDataLoaded,
|
||||
IWorkflowStatisticsTimestamps,
|
||||
ResponseHelper,
|
||||
} from '..';
|
||||
import { StatisticsNames } from '../databases/entities/WorkflowStatistics';
|
||||
import { getLogger } from '../Logger';
|
||||
import { ExecutionRequest } from '../requests';
|
||||
|
||||
export const workflowStatsController = express.Router();
|
||||
|
||||
// Helper function that validates the ID, return a flag stating whether the request is allowed
|
||||
async function checkWorkflowId(workflowId: string, user: User): Promise<boolean> {
|
||||
// Check permissions
|
||||
const shared = await Db.collections.SharedWorkflow.findOne({
|
||||
relations: ['workflow'],
|
||||
where: whereClause({
|
||||
user,
|
||||
entityType: 'workflow',
|
||||
entityId: workflowId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!shared) {
|
||||
LoggerProxy.info('User attempted to read a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: user.id,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise Logger if needed
|
||||
*/
|
||||
workflowStatsController.use((req, res, next) => {
|
||||
try {
|
||||
LoggerProxy.getInstance();
|
||||
} catch (error) {
|
||||
LoggerProxy.init(getLogger());
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Check that the workflow ID is valid and allowed to be read by the user
|
||||
*/
|
||||
workflowStatsController.use(async (req: ExecutionRequest.Get, res, next) => {
|
||||
const allowed = await checkWorkflowId(req.params.id, req.user);
|
||||
if (allowed) {
|
||||
next();
|
||||
} else {
|
||||
// Otherwise, make and return an error
|
||||
const response = new ResponseHelper.NotFoundError(`Workflow ${req.params.id} does not exist.`);
|
||||
next(response);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /workflow-stats/:id/counts/
|
||||
*/
|
||||
workflowStatsController.get(
|
||||
'/:id/counts/',
|
||||
ResponseHelper.send(async (req: ExecutionRequest.Get): Promise<IWorkflowStatisticsCounts> => {
|
||||
// Get counts from DB
|
||||
const workflowId = req.params.id;
|
||||
|
||||
// Find the stats for this workflow
|
||||
const stats = await Db.collections.WorkflowStatistics.find({
|
||||
select: ['count', 'name'],
|
||||
where: {
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
|
||||
const data: IWorkflowStatisticsCounts = {
|
||||
productionSuccess: 0,
|
||||
productionError: 0,
|
||||
manualSuccess: 0,
|
||||
manualError: 0,
|
||||
};
|
||||
|
||||
// There will be a maximum of 4 stats (currently)
|
||||
stats.forEach(({ count, name }) => {
|
||||
switch (name) {
|
||||
case StatisticsNames.manualError:
|
||||
data.manualError = count;
|
||||
break;
|
||||
|
||||
case StatisticsNames.manualSuccess:
|
||||
data.manualSuccess = count;
|
||||
break;
|
||||
|
||||
case StatisticsNames.productionError:
|
||||
data.productionError = count;
|
||||
break;
|
||||
|
||||
case StatisticsNames.productionSuccess:
|
||||
data.productionSuccess = count;
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /workflow-stats/:id/times/
|
||||
*/
|
||||
workflowStatsController.get(
|
||||
'/:id/times/',
|
||||
ResponseHelper.send(async (req: ExecutionRequest.Get): Promise<IWorkflowStatisticsTimestamps> => {
|
||||
// Get times from DB
|
||||
const workflowId = req.params.id;
|
||||
|
||||
// Find the stats for this workflow
|
||||
const stats = await Db.collections.WorkflowStatistics.find({
|
||||
select: ['latestEvent', 'name'],
|
||||
where: {
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
|
||||
const data: IWorkflowStatisticsTimestamps = {
|
||||
productionSuccess: null,
|
||||
productionError: null,
|
||||
manualSuccess: null,
|
||||
manualError: null,
|
||||
};
|
||||
|
||||
// There will be a maximum of 4 stats (currently)
|
||||
stats.forEach(({ latestEvent, name }) => {
|
||||
switch (name) {
|
||||
case StatisticsNames.manualError:
|
||||
data.manualError = latestEvent;
|
||||
break;
|
||||
|
||||
case StatisticsNames.manualSuccess:
|
||||
data.manualSuccess = latestEvent;
|
||||
break;
|
||||
|
||||
case StatisticsNames.productionError:
|
||||
data.productionError = latestEvent;
|
||||
break;
|
||||
|
||||
case StatisticsNames.productionSuccess:
|
||||
data.productionSuccess = latestEvent;
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /workflow-stats/:id/data-loaded/
|
||||
*/
|
||||
workflowStatsController.get(
|
||||
'/:id/data-loaded/',
|
||||
ResponseHelper.send(async (req: ExecutionRequest.Get): Promise<IWorkflowStatisticsDataLoaded> => {
|
||||
// Get flag
|
||||
const workflowId = req.params.id;
|
||||
|
||||
// Get the corresponding workflow
|
||||
const workflow = await Db.collections.Workflow.findOne(workflowId);
|
||||
// It will be valid if we reach this point, this is just for TS
|
||||
if (!workflow) {
|
||||
return { dataLoaded: false };
|
||||
}
|
||||
|
||||
const data: IWorkflowStatisticsDataLoaded = {
|
||||
dataLoaded: workflow.dataLoaded,
|
||||
};
|
||||
|
||||
return data;
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user