Skip to content

Commit b98672a

Browse files
authored
Merge pull request #117 from FlowTestAI/cli-logs-1
feat: Define logger and ability to upload build scan for cli
2 parents e71931d + 10e0527 commit b98672a

10 files changed

Lines changed: 192 additions & 25 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// lib/axiosClient.ts
2+
const axios = require('axios');
3+
const axiosRetry = require('axios-retry').default;
4+
5+
const baseUrl = 'http://localhost:3000';
6+
7+
const axiosClient = axios.create({
8+
baseURL: `${baseUrl}/api`,
9+
headers: {
10+
'Content-Type': 'application/json',
11+
},
12+
});
13+
14+
axiosRetry(axiosClient, {
15+
retries: 3, // Number of retries
16+
retryDelay: (retryCount) => {
17+
return retryCount * 1000; // Time interval between retries (1000 ms = 1 second)
18+
},
19+
retryCondition: (error) => {
20+
// Retry on network errors or 5xx server errors
21+
return error.response?.status === 500 || error.code === 'ECONNABORTED';
22+
},
23+
});
24+
25+
module.exports = {
26+
baseUrl,
27+
axiosClient,
28+
};

packages/flowtest-cli/bin/index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const { serialize } = require('../../flowtest-electron/src/utils/flowparser/pars
88
const { Graph } = require('../graph/Graph');
99
const { cloneDeep } = require('lodash');
1010
const dotenv = require('dotenv');
11+
const { GraphLogger, LogLevel } = require('../graph/GraphLogger');
12+
const { baseUrl, axiosClient } = require('./axiosClient');
1113

1214
const getEnvVariables = (pathname) => {
1315
const content = readFile(pathname);
@@ -16,6 +18,11 @@ const getEnvVariables = (pathname) => {
1618
return parsed;
1719
};
1820

21+
function bytesToBase64(bytes) {
22+
const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');
23+
return btoa(binString);
24+
}
25+
1926
// Define the CLI application using yargs
2027
const argv = yargs(hideBin(process.argv))
2128
.usage('Usage: $0 <command> [options]')
@@ -41,6 +48,10 @@ const argv = yargs(hideBin(process.argv))
4148
describe: 'timeout for graph run in ms',
4249
demandOption: false,
4350
type: 'number',
51+
})
52+
.option('scan', {
53+
alias: 's',
54+
describe: 'generate and upload build scan',
4455
});
4556
},
4657
async (argv) => {
@@ -57,13 +68,15 @@ const argv = yargs(hideBin(process.argv))
5768
const flowData = serialize(JSON.parse(content));
5869
// output json output to a file
5970
//console.log(chalk.green(JSON.stringify(flowData)));
71+
const logger = new GraphLogger();
6072
const startTime = Date.now();
6173
const g = new Graph(
6274
cloneDeep(flowData.nodes),
6375
cloneDeep(flowData.edges),
6476
startTime,
6577
argv.timeout ? argv.timeout : 60000,
6678
argv.env ? getEnvVariables(argv.env) : {},
79+
logger,
6780
);
6881
console.log(chalk.yellow('Running Graph \n'));
6982
if (flowData.nodes.find((n) => n.type === 'complexNode')) {
@@ -80,7 +93,28 @@ const argv = yargs(hideBin(process.argv))
8093
} else {
8194
console.log(chalk.bold('Graph Run: ') + chalk.red(` ✕ `) + chalk.dim(result.status));
8295
}
96+
logger.add(LogLevel.INFO, `Total time: ${Date.now() - startTime} ms`);
8397
console.log(chalk.bold('Total Time: ') + chalk.dim(`${Date.now() - startTime} ms`));
98+
//console.log(logger.get());
99+
100+
if (argv.scan) {
101+
const data = {
102+
version: 1,
103+
name: argv.file.toString(),
104+
scan: logger.get(),
105+
};
106+
try {
107+
const response = await axiosClient.post(
108+
'/upload',
109+
bytesToBase64(new TextEncoder().encode(JSON.stringify(data))),
110+
);
111+
console.log(chalk.bold('Build Scan: ') + chalk.dim(`${baseUrl}/scan/${response.data.data[0].id}`));
112+
} catch (error) {
113+
//console.log(error);
114+
console.log(chalk.red(` ✕ `) + chalk.dim('Unable to upload build scan'));
115+
}
116+
}
117+
84118
process.exit(1);
85119
//console.log(chalk.green(JSON.stringify(result)));
86120
} catch (error) {

packages/flowtest-cli/graph/Graph.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ const path = require('path');
1010
const readFile = require('../../flowtest-electron/src/utils/filemanager/readfile');
1111
const { serialize } = require('../../flowtest-electron/src/utils/flowparser/parser');
1212
const Node = require('./compute/node');
13+
const { LogLevel } = require('./GraphLogger');
1314

1415
class nestedFlowNode extends Node {
15-
constructor(nodes, edges, startTime, timeout, initialEnvVars) {
16+
constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) {
1617
super('flowNode');
1718
try {
18-
this.internalGraph = new Graph(nodes, edges, startTime, timeout, initialEnvVars);
19+
this.internalGraph = new Graph(nodes, edges, startTime, timeout, initialEnvVars, logger);
1920
} catch (error) {
2021
console.log(error);
2122
}
@@ -27,14 +28,15 @@ class nestedFlowNode extends Node {
2728
}
2829

2930
class Graph {
30-
constructor(nodes, edges, startTime, timeout, initialEnvVars) {
31+
constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) {
3132
this.nodes = nodes;
3233
this.edges = edges;
3334
this.timeout = timeout;
3435
this.startTime = startTime;
3536
this.graphRunNodeOutput = {};
3637
this.auth = undefined;
3738
this.envVariables = initialEnvVars;
39+
this.logger = logger;
3840
}
3941

4042
#checkTimeout() {
@@ -83,6 +85,7 @@ class Graph {
8385
if (node.type === 'outputNode') {
8486
console.log('Output Node');
8587
console.log(chalk.green(` ✓ `) + chalk.dim(`${JSON.stringify(prevNodeOutputData)}`));
88+
this.logger.add(LogLevel.INFO, '', { type: 'outputNode', data: { output: prevNodeOutputData } });
8689
result = {
8790
status: 'Success',
8891
data: prevNodeOutputData,
@@ -95,7 +98,7 @@ class Graph {
9598
node.data.variables,
9699
prevNodeOutputData,
97100
this.envVariables,
98-
this.logs,
101+
this.logger,
99102
);
100103
if (eNode.evaluate()) {
101104
console.log(chalk.green(` ✓ `) + chalk.dim('True'));
@@ -121,6 +124,7 @@ class Graph {
121124
};
122125
console.log('Delay Node: ' + chalk.green(`....waiting for: ${delay} ms`));
123126
await wait(delay);
127+
this.logger.add(LogLevel.INFO, '', { type: 'delayNode', data: { delay } });
124128
result = {
125129
status: 'Success',
126130
};
@@ -137,7 +141,7 @@ class Graph {
137141

138142
if (node.type === 'requestNode') {
139143
console.log('Request Node');
140-
const rNode = new requestNode(node.data, prevNodeOutputData, this.envVariables, this.auth);
144+
const rNode = new requestNode(node.data, prevNodeOutputData, this.envVariables, this.auth, this.logger);
141145
result = await rNode.evaluate();
142146
// add post response variables if any
143147
if (result.postRespVars) {
@@ -159,6 +163,7 @@ class Graph {
159163
this.startTime,
160164
this.timeout,
161165
this.envVariables,
166+
this.logger,
162167
);
163168
result = await cNode.evaluate();
164169
this.envVariables = result.envVars;
@@ -175,6 +180,13 @@ class Graph {
175180
const newVariable = sNode.evaluate();
176181
if (newVariable != undefined) {
177182
console.log(chalk.green(` ✓ `) + chalk.dim(`Set variable: ${JSON.stringify(newVariable)}`));
183+
this.logger.add(LogLevel.INFO, '', {
184+
type: 'setVarNode',
185+
data: {
186+
name: Object.keys(newVariable)[0],
187+
value: newVariable[Object.keys(newVariable)[0]],
188+
},
189+
});
178190
this.envVariables = {
179191
...this.envVariables,
180192
...newVariable,
@@ -186,17 +198,19 @@ class Graph {
186198
}
187199

188200
if (this.#checkTimeout()) {
189-
throw `Timeout of ${this.timeout} ms exceeded, stopping graph run`;
201+
throw Error(`Timeout of ${this.timeout} ms exceeded, stopping graph run`);
190202
}
191203
} catch (err) {
192204
console.log(chalk.red(`Flow failed at: ${JSON.stringify(node.data)} due to ${err}`));
205+
this.logger.add(LogLevel.ERROR, `Flow failed due to ${err}`, node);
193206
return {
194207
status: 'Failed',
195208
};
196209
}
197210

198211
if (result === undefined) {
199212
console.log(chalk.red(`Flow failed due to failure to evaluate result at node: ${node.data}`));
213+
this.logger.add(LogLevel.ERROR, 'Flow failed due to failure to evaluate result', node);
200214
return {
201215
status: 'Failed',
202216
};
@@ -222,10 +236,14 @@ class Graph {
222236
this.graphRunNodeOutput = {};
223237

224238
console.log(chalk.green('Start Flowtest'));
239+
this.logger.add(LogLevel.INFO, 'Start Flowtest');
240+
225241
const startNode = this.nodes.find((node) => node.type === 'startNode');
226242
if (startNode == undefined) {
227243
console.log(chalk.red(`✕ `) + chalk.red('No start node found'));
228244
console.log(chalk.red('End Flowtest'));
245+
this.logger.add(LogLevel.INFO, 'No start node found');
246+
this.logger.add(LogLevel.INFO, 'End Flowtest');
229247
return {
230248
status: 'Success',
231249
envVars: this.envVariables,
@@ -242,12 +260,14 @@ class Graph {
242260
} else {
243261
console.log(chalk.green('End Flowtest'));
244262
}
263+
this.logger.add(LogLevel.INFO, 'End Flowtest');
245264
return {
246265
status: result.status,
247266
envVars: this.envVariables,
248267
};
249268
} else {
250269
console.log(chalk.green('End Flowtest'));
270+
this.logger.add(LogLevel.INFO, 'End Flowtest');
251271
return {
252272
status: 'Success',
253273
envVars: this.envVariables,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const LogLevel = Object.freeze({
2+
INFO: 'info',
3+
WARN: 'warn',
4+
ERROR: 'error',
5+
});
6+
7+
class GraphLogger {
8+
constructor() {
9+
this.logs = [];
10+
}
11+
12+
add(logLevel, message, node) {
13+
this.logs.push({
14+
level: logLevel,
15+
timestamp: new Date().toISOString(),
16+
message,
17+
node,
18+
});
19+
}
20+
21+
get() {
22+
return this.logs;
23+
}
24+
}
25+
26+
module.exports = {
27+
GraphLogger,
28+
LogLevel,
29+
};

packages/flowtest-cli/graph/compute/assertnode.js

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ const AssertOperators = require('../constants/assertOperators');
22
const { computeNodeVariable } = require('./utils');
33
const Node = require('./node');
44
const chalk = require('chalk');
5+
const { LogLevel } = require('../GraphLogger');
56

67
class assertNode extends Node {
7-
constructor(operator, variables, prevNodeOutputData, envVariables, logs) {
8+
constructor(operator, variables, prevNodeOutputData, envVariables, logger) {
89
super('assertNode');
910
this.operator = operator;
1011
this.variables = variables;
1112
this.prevNodeOutputData = prevNodeOutputData;
12-
this.logs = logs;
13+
this.logger = logger;
1314
this.envVariables = envVariables;
1415
}
1516

@@ -32,7 +33,7 @@ class assertNode extends Node {
3233

3334
const operator = this.operator;
3435
if (operator == undefined) {
35-
throw 'Operator undefined';
36+
throw Error('Operator undefined');
3637
}
3738
// this.logs.push(
3839
// `Assert var1: ${JSON.stringify(var1)} of type: ${typeof var1}, var2: ${JSON.stringify(var2)} of type: ${typeof var2} with operator: ${operator}`,
@@ -43,17 +44,26 @@ class assertNode extends Node {
4344
`Assert var1: ${JSON.stringify(var1)} of type: ${typeof var1}, var2: ${JSON.stringify(var2)} of type: ${typeof var2} with operator: ${operator}`,
4445
),
4546
);
46-
if (operator == AssertOperators.isEqualTo) {
47-
return var1 === var2;
48-
} else if (operator == AssertOperators.isNotEqualTo) {
49-
return var1 != var2;
50-
} else if (operator == AssertOperators.isGreaterThan) {
51-
return var1 > var2;
52-
} else if (operator == AssertOperators.isLessThan) {
53-
return var1 < var2;
54-
} else {
55-
throw 'Operator unrecognized';
47+
let result;
48+
switch (operator) {
49+
case AssertOperators.isEqualTo:
50+
result = var1 === var2;
51+
break;
52+
case AssertOperators.isNotEqualTo:
53+
result = var1 != var2;
54+
break;
55+
case AssertOperators.isGreaterThan:
56+
result = var1 > var2;
57+
break;
58+
case AssertOperators.isLessThan:
59+
result = var1 < var2;
60+
break;
61+
default:
62+
throw Error('Unsupported operator');
5663
}
64+
this.logger.add(LogLevel.INFO, '', { type: 'assertNode', data: { var1, var2, operator, result } });
65+
66+
return result;
5767
}
5868
}
5969

packages/flowtest-cli/graph/compute/authnode.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@ const Node = require('./node');
33
const chalk = require('chalk');
44

55
class authNode extends Node {
6-
constructor(auth, envVariables) {
6+
constructor(auth, envVariables, logger) {
77
super('authNode');
88
(this.auth = auth), (this.envVariables = envVariables);
9+
this.logger = logger;
910
}
1011

1112
evaluate() {
1213
//console.log('Evaluating an auth node');
1314
if (this.auth.type === 'basic-auth') {
1415
console.log(chalk.green(` ✓ `) + chalk.dim('.....setting basic authentication'));
16+
this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'Basic Authentication' } });
1517
this.auth.username = computeVariables(this.auth.username, this.envVariables);
1618
this.auth.password = computeVariables(this.auth.password, this.envVariables);
1719

1820
return this.auth;
1921
} else if (this.auth.type === 'no-auth') {
2022
console.log(chalk.green(` ✓ `) + chalk.dim('.....using no authentication'));
23+
this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'No Authentication' } });
2124
return this.auth;
2225
} else {
2326
throw Error(`auth type: ${this.auth.type} is not valid`);

0 commit comments

Comments
 (0)