feat(plugin-workflow): add workflow specific logger (#1677)

* feat(plugin-workflow): add workflow specific logger

* fix(plugin-workflow): fix packages

* refactor(logger): adjust logger path env
This commit is contained in:
Junyi 2023-04-10 20:00:29 +07:00 committed by GitHub
parent cd59cd8568
commit 0126a48cfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 85 additions and 22 deletions

View File

@ -19,6 +19,10 @@ API_BASE_URL=
PROXY_TARGET_URL= PROXY_TARGET_URL=
LOGGER_TRANSPORT=
LOGGER_LEVEL=
LOGGER_BASE_PATH=storage/logs
################# DATABASE ################# ################# DATABASE #################
DB_DIALECT=sqlite DB_DIALECT=sqlite

View File

@ -159,9 +159,9 @@ Log transport, default is `console,dailyRotateFile`, options include
- `console` - `console`
- `dailyRotateFile` - `dailyRotateFile`
### DAILY_ROTATE_FILE_DIRNAME ### LOGGER_BASE_PATH
Path to save `dailyRotateFile` logs, default is `storage/logs` Base path to save file based logs, default is `storage/logs`
## Temporary Environment Variables ## Temporary Environment Variables

View File

@ -18,7 +18,7 @@ app.use(middleware);
logger 相关环境变量有: logger 相关环境变量有:
- [LOGGER_TRANSPORT](/api/env#logger_transport) - [LOGGER_TRANSPORT](/api/env#logger_transport)
- [DAILY_ROTATE_FILE_DIRNAME](/api/env#daily_rotate_file_dirname) - [LOGGER_BASE_PATH](/api/env#logger_base_path)
## Application 的 logger 配置 ## Application 的 logger 配置

View File

@ -154,14 +154,14 @@ DB_LOGGING=on
### LOGGER_TRANSPORT ### LOGGER_TRANSPORT
日志 transport默认值 `console,dailyRotateFile`,可选项 日志 transport默认值 `console,dailyRotateFile`,可选项
- `console` - `console`
- `dailyRotateFile` - `dailyRotateFile`
### DAILY_ROTATE_FILE_DIRNAME ### LOGGER_BASE_PATH
`dailyRotateFile` 日志的存储路径,默认为 `storage/logs` 基于文件的日志存储路径,默认为 `storage/logs`
## 临时环境变量 ## 临时环境变量

View File

@ -18,7 +18,7 @@ app.use(middleware);
logger 相关环境变量有: logger 相关环境变量有:
- [LOGGER_TRANSPORT](/api/env#logger_transport) - [LOGGER_TRANSPORT](/api/env#logger_transport)
- [DAILY_ROTATE_FILE_DIRNAME](/api/env#daily_rotate_file_dirname) - [LOGGER_BASE_PATH](/api/env#logger_base_path)
## Application 的 logger 配置 ## Application 的 logger 配置

View File

@ -4,10 +4,14 @@ import 'winston-daily-rotate-file';
const { combine, timestamp, colorize, simple } = format; const { combine, timestamp, colorize, simple } = format;
function loggingLevel() { export function getLoggerLevel(): string {
return process.env.LOGGER_LEVEL || 'info'; return process.env.LOGGER_LEVEL || 'info';
} }
export function getLoggerFilePath(...paths: string[]): string {
return path.resolve(process.env.LOGGER_BASE_PATH || path.resolve(process.cwd(), 'storage', 'logs'), ...paths);
}
const Transports = { const Transports = {
console(options) { console(options) {
return new winston.transports.Console({ return new winston.transports.Console({
@ -16,13 +20,13 @@ const Transports = {
}); });
}, },
dailyRotateFile(options: any) { dailyRotateFile(options: any) {
let dirname = process.env.DAILY_ROTATE_FILE_DIRNAME || path.resolve(process.cwd(), './storage/logs'); let dirname = getLoggerFilePath();
if (!path.isAbsolute(dirname)) { if (!path.isAbsolute(dirname)) {
dirname = path.resolve(process.cwd(), dirname); dirname = path.resolve(process.cwd(), dirname);
} }
return new winston.transports.DailyRotateFile({ return new winston.transports.DailyRotateFile({
dirname, dirname,
level: loggingLevel(), level: getLoggerLevel(),
filename: 'nocobase-%DATE%.log', filename: 'nocobase-%DATE%.log',
datePattern: 'YYYY-MM-DD-HH', datePattern: 'YYYY-MM-DD-HH',
maxFiles: '14d', maxFiles: '14d',
@ -54,7 +58,7 @@ function createLogger(options: LoggerOptions = {}) {
}) })
.filter((t) => t); .filter((t) => t);
const logger = winston.createLogger({ const logger = winston.createLogger({
level: loggingLevel(), level: getLoggerLevel(),
levels: winston.config.cli.levels, levels: winston.config.cli.levels,
format: combine(timestamp(), format.errors({ stack: true }), format.json(), colorize()), format: combine(timestamp(), format.errors({ stack: true }), format.json(), colorize()),
...options, ...options,

View File

@ -10,6 +10,7 @@
"@nocobase/client": "0.9.1-alpha.2", "@nocobase/client": "0.9.1-alpha.2",
"@nocobase/database": "0.9.1-alpha.2", "@nocobase/database": "0.9.1-alpha.2",
"@nocobase/evaluators": "0.9.1-alpha.2", "@nocobase/evaluators": "0.9.1-alpha.2",
"@nocobase/logger": "0.9.1-alpha.2",
"@nocobase/resourcer": "0.9.1-alpha.2", "@nocobase/resourcer": "0.9.1-alpha.2",
"@nocobase/server": "0.9.1-alpha.2", "@nocobase/server": "0.9.1-alpha.2",
"@nocobase/utils": "0.9.1-alpha.2", "@nocobase/utils": "0.9.1-alpha.2",
@ -18,6 +19,7 @@
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",
"json-templates": "^4.2.0", "json-templates": "^4.2.0",
"lru-cache": "8.0.5",
"moment": "^2.29.2", "moment": "^2.29.2",
"react-js-cron": "^3.1.0" "react-js-cron": "^3.1.0"
}, },

View File

@ -1,5 +1,8 @@
import path from 'path'; import path from 'path';
import winston from 'winston';
import LRUCache from 'lru-cache';
import { Op } from '@nocobase/database'; import { Op } from '@nocobase/database';
import { Plugin } from '@nocobase/server'; import { Plugin } from '@nocobase/server';
import { Registry } from '@nocobase/utils'; import { Registry } from '@nocobase/utils';
@ -14,15 +17,43 @@ import WorkflowModel from './models/Workflow';
import Processor from './Processor'; import Processor from './Processor';
import initTriggers, { Trigger } from './triggers'; import initTriggers, { Trigger } from './triggers';
import initFunctions from './functions'; import initFunctions from './functions';
import { createLogger, Logger, LoggerOptions, getLoggerLevel, getLoggerFilePath } from '@nocobase/logger';
type Pending = [ExecutionModel, JobModel?]; type Pending = [ExecutionModel, JobModel?];
type ID = number | string;
export default class WorkflowPlugin extends Plugin { export default class WorkflowPlugin extends Plugin {
instructions: Registry<Instruction> = new Registry(); instructions: Registry<Instruction> = new Registry();
triggers: Registry<Trigger> = new Registry(); triggers: Registry<Trigger> = new Registry();
functions: Registry<Function> = new Registry(); functions: Registry<Function> = new Registry();
executing: ExecutionModel | null = null; private executing: ExecutionModel | null = null;
pending: Pending[] = []; private pending: Pending[] = [];
events: [WorkflowModel, any, { context?: any }][] = []; private events: [WorkflowModel, any, { context?: any }][] = [];
private loggerCache: LRUCache<string, Logger>;
getLogger(workflowId: ID): Logger {
const now = new Date();
const date = `${now.getFullYear()}-${`0${now.getMonth() + 1}`.slice(-2)}-${`0${now.getDate()}`.slice(-2)}`;
const key = `${date}-${workflowId}}`;
if (this.loggerCache.has(key)) {
return this.loggerCache.get(key);
}
const logger = createLogger({
transports: [
'console',
new winston.transports.File({
filename: getLoggerFilePath('workflows', date, `${workflowId}.log`),
level: getLoggerLevel(),
})
],
} as LoggerOptions);
this.loggerCache.set(key, logger);
return logger;
}
onBeforeSave = async (instance: WorkflowModel, options) => { onBeforeSave = async (instance: WorkflowModel, options) => {
const Model = <typeof WorkflowModel>instance.constructor; const Model = <typeof WorkflowModel>instance.constructor;
@ -79,6 +110,14 @@ export default class WorkflowPlugin extends Plugin {
initInstructions(this, options.instructions); initInstructions(this, options.instructions);
initFunctions(this, options.functions); initFunctions(this, options.functions);
this.loggerCache = new LRUCache({
max: 20,
updateAgeOnGet: true,
dispose(logger) {
(<Logger>logger).end();
}
});
this.app.acl.registerSnippet({ this.app.acl.registerSnippet({
name: `pm.${this.name}.workflows`, name: `pm.${this.name}.workflows`,
actions: [ actions: [
@ -162,7 +201,7 @@ export default class WorkflowPlugin extends Plugin {
this.events.push([workflow, context, options]); this.events.push([workflow, context, options]);
this.app.logger.debug(`[Workflow] new event triggered, now events: ${this.events.length}`); this.getLogger(workflow.id).debug(`new event triggered, now events: ${this.events.length}`, { data: workflow.config });
if (this.events.length > 1) { if (this.events.length > 1) {
return; return;
@ -189,9 +228,8 @@ export default class WorkflowPlugin extends Plugin {
}); });
if (existed) { if (existed) {
this.app.logger.warn( this.getLogger(workflow.id).warn(`workflow ${workflow.id} has already been triggered in same execution (${options.context.executionId}), and newly triggering will be skipped.`);
`[Workflow] workflow ${workflow.id} has already been triggered in same execution (${options.context.executionId}), and newly triggering will be skipped.`,
);
valid = false; valid = false;
} }
} }
@ -234,7 +272,7 @@ export default class WorkflowPlugin extends Plugin {
return execution; return execution;
}); });
this.app.logger.debug(`[Workflow] execution of workflow ${workflow.id} created as ${execution.id}`); this.getLogger(workflow.id).debug(`execution of workflow ${workflow.id} created as ${execution.id}`, { data: execution.context });
// NOTE: cache first execution for most cases // NOTE: cache first execution for most cases
if (!this.executing && !this.pending.length) { if (!this.executing && !this.pending.length) {
@ -292,12 +330,13 @@ export default class WorkflowPlugin extends Plugin {
const processor = this.createProcessor(execution); const processor = this.createProcessor(execution);
this.app.logger.info(`[Workflow] execution ${execution.id} ${job ? 'resuming' : 'starting'}...`); this.getLogger(execution.workflowId).info(`execution (${execution.id}) ${job ? 'resuming' : 'starting'}...`);
try { try {
await (job ? processor.resume(job) : processor.start()); await (job ? processor.resume(job) : processor.start());
this.getLogger(execution.workflowId).info(`execution (${execution.id}) finished with status: ${execution.status}`);
} catch (err) { } catch (err) {
this.app.logger.error(`[Workflow] ${err.message}`, err); this.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
} }
this.executing = null; this.executing = null;

View File

@ -9,6 +9,7 @@ import ExecutionModel from './models/Execution';
import JobModel from './models/Job'; import JobModel from './models/Job';
import FlowNodeModel from './models/FlowNode'; import FlowNodeModel from './models/FlowNode';
import { EXECUTION_STATUS, JOB_STATUS } from './constants'; import { EXECUTION_STATUS, JOB_STATUS } from './constants';
import { Logger } from '@nocobase/logger';
@ -29,6 +30,8 @@ export default class Processor {
[JOB_STATUS.REJECTED]: EXECUTION_STATUS.REJECTED, [JOB_STATUS.REJECTED]: EXECUTION_STATUS.REJECTED,
}; };
logger: Logger;
transaction?: Transaction; transaction?: Transaction;
nodes: FlowNodeModel[] = []; nodes: FlowNodeModel[] = [];
@ -37,6 +40,7 @@ export default class Processor {
jobsMapByNodeId: { [key: number]: any } = {}; jobsMapByNodeId: { [key: number]: any } = {};
constructor(public execution: ExecutionModel, public options: ProcessorOptions) { constructor(public execution: ExecutionModel, public options: ProcessorOptions) {
this.logger = options.plugin.getLogger(execution.workflowId);
} }
// make dual linked nodes list then cache // make dual linked nodes list then cache
@ -137,12 +141,15 @@ export default class Processor {
let job; let job;
try { try {
// call instruction to get result and status // call instruction to get result and status
this.logger.info(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id})`);
this.logger.debug(`config of node`, { data: node.config });
job = await instruction(node, prevJob, this); job = await instruction(node, prevJob, this);
if (!job) { if (!job) {
return null; return null;
} }
} catch (err) { } catch (err) {
// for uncaught error, set to error // for uncaught error, set to error
this.logger.error(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) failed: `, { error: err });
job = { job = {
result: err instanceof Error result: err instanceof Error
? { message: err.message, stack: process.env.NODE_ENV === 'production' ? [] : err.stack } ? { message: err.message, stack: process.env.NODE_ENV === 'production' ? [] : err.stack }
@ -162,8 +169,12 @@ export default class Processor {
} }
const savedJob = await this.saveJob(job); const savedJob = await this.saveJob(job);
this.logger.info(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) finished as status: ${savedJob.status}`);
this.logger.debug(`result of node`, { data: savedJob.result });
if (savedJob.status === JOB_STATUS.RESOLVED && node.downstream) { if (savedJob.status === JOB_STATUS.RESOLVED && node.downstream) {
// run next node // run next node
this.logger.debug(`run next node (${node.id})`);
return this.run(node.downstream, savedJob); return this.run(node.downstream, savedJob);
} }
@ -183,9 +194,11 @@ export default class Processor {
// parent node should take over the control // parent node should take over the control
public async end(node, job) { public async end(node, job) {
this.logger.debug(`branch ended at node (${node.id})})`);
const parentNode = this.findBranchParentNode(node); const parentNode = this.findBranchParentNode(node);
// no parent, means on main flow // no parent, means on main flow
if (parentNode) { if (parentNode) {
this.logger.debug(`not on main, recall to parent entry node (${node.id})})`);
await this.recall(parentNode, job); await this.recall(parentNode, job);
return job; return job;
} }
@ -207,6 +220,7 @@ export default class Processor {
async exit(job: JobModel | null) { async exit(job: JobModel | null) {
const status = job ? (<typeof Processor>this.constructor).StatusMap[job.status] ?? Math.sign(job.status) : EXECUTION_STATUS.RESOLVED; const status = job ? (<typeof Processor>this.constructor).StatusMap[job.status] ?? Math.sign(job.status) : EXECUTION_STATUS.RESOLVED;
this.logger.info(`execution (${this.execution.id}) all nodes finished, finishing execution...`);
await this.execution.update({ status }, { transaction: this.transaction }); await this.execution.update({ status }, { transaction: this.transaction });
return null; return null;
} }

View File

@ -38,7 +38,7 @@ export default function () {
{ {
type: 'boolean', type: 'boolean',
name: 'useTransaction', name: 'useTransaction',
defaultValue: true, // defaultValue: true,
}, },
{ {
type: 'hasMany', type: 'hasMany',