Add console logs to code step (#10786)
Works for local and lambda drivers ## After  
This commit is contained in:
@ -36,7 +36,7 @@ const documents = {
|
|||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n logs\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
"\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.PublishOneServerlessFunctionDocument,
|
"\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.PublishOneServerlessFunctionDocument,
|
||||||
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
||||||
"\n query FindManyAvailablePackages($input: ServerlessFunctionIdInput!) {\n getAvailablePackages(input: $input)\n }\n": types.FindManyAvailablePackagesDocument,
|
"\n query FindManyAvailablePackages($input: ServerlessFunctionIdInput!) {\n getAvailablePackages(input: $input)\n }\n": types.FindManyAvailablePackagesDocument,
|
||||||
@ -154,7 +154,7 @@ export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($i
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n"];
|
export function graphql(source: "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n logs\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n logs\n duration\n status\n error\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -45,6 +45,7 @@ export const useTestServerlessFunction = ({
|
|||||||
4,
|
4,
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
logs: result?.data?.executeOneServerlessFunction?.logs || '',
|
||||||
duration: result?.data?.executeOneServerlessFunction?.duration,
|
duration: result?.data?.executeOneServerlessFunction?.duration,
|
||||||
status: result?.data?.executeOneServerlessFunction?.status,
|
status: result?.data?.executeOneServerlessFunction?.status,
|
||||||
error: result?.data?.executeOneServerlessFunction?.error
|
error: result?.data?.executeOneServerlessFunction?.error
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
|||||||
) {
|
) {
|
||||||
executeOneServerlessFunction(input: $input) {
|
executeOneServerlessFunction(input: $input) {
|
||||||
data
|
data
|
||||||
|
logs
|
||||||
duration
|
duration
|
||||||
status
|
status
|
||||||
error
|
error
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type TextAreaProps = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
minRows?: number;
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -73,12 +74,13 @@ export const TextArea = ({
|
|||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
minRows = 1,
|
minRows = 1,
|
||||||
|
maxRows = MAX_ROWS,
|
||||||
value = '',
|
value = '',
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
const computedMinRows = Math.min(minRows, MAX_ROWS);
|
const computedMinRows = Math.min(minRows, maxRows);
|
||||||
|
|
||||||
const inputId = useId();
|
const inputId = useId();
|
||||||
|
|
||||||
@ -103,7 +105,7 @@ export const TextArea = ({
|
|||||||
<StyledTextArea
|
<StyledTextArea
|
||||||
id={inputId}
|
id={inputId}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
maxRows={MAX_ROWS}
|
maxRows={maxRows}
|
||||||
minRows={computedMinRows}
|
minRows={computedMinRows}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export type ServerlessFunctionTestData = {
|
|||||||
input: { [field: string]: any };
|
input: { [field: string]: any };
|
||||||
output: {
|
output: {
|
||||||
data?: string;
|
data?: string;
|
||||||
|
logs: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
status?: ServerlessFunctionExecutionStatus;
|
status?: ServerlessFunctionExecutionStatus;
|
||||||
error?: string;
|
error?: string;
|
||||||
@ -15,6 +16,7 @@ export type ServerlessFunctionTestData = {
|
|||||||
|
|
||||||
export const DEFAULT_OUTPUT_VALUE = {
|
export const DEFAULT_OUTPUT_VALUE = {
|
||||||
data: 'Enter an input above then press "Test"',
|
data: 'Enter an input above then press "Test"',
|
||||||
|
logs: '',
|
||||||
status: ServerlessFunctionExecutionStatus.IDLE,
|
status: ServerlessFunctionExecutionStatus.IDLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { CodeEditor, IconCode, IconPlayerPlay, useIcons } from 'twenty-ui';
|
import { CodeEditor, IconCode, IconPlayerPlay, useIcons } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -340,6 +341,16 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
isTesting={isTesting}
|
isTesting={isTesting}
|
||||||
/>
|
/>
|
||||||
</StyledCodeEditorContainer>
|
</StyledCodeEditorContainer>
|
||||||
|
<StyledCodeEditorContainer>
|
||||||
|
<InputLabel>Logs</InputLabel>
|
||||||
|
<TextArea
|
||||||
|
value={
|
||||||
|
isTesting ? '' : serverlessFunctionTestData.output.logs
|
||||||
|
}
|
||||||
|
maxRows={20}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</StyledCodeEditorContainer>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</WorkflowStepBody>
|
</WorkflowStepBody>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export type ServerlessExecuteError = {
|
|||||||
export type ServerlessExecuteResult = {
|
export type ServerlessExecuteResult = {
|
||||||
data: object | null;
|
data: object | null;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
logs: string;
|
||||||
status: ServerlessFunctionExecutionStatus;
|
status: ServerlessFunctionExecutionStatus;
|
||||||
error?: ServerlessExecuteError;
|
error?: ServerlessExecuteError;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
PublishLayerVersionCommandInput,
|
PublishLayerVersionCommandInput,
|
||||||
ResourceNotFoundException,
|
ResourceNotFoundException,
|
||||||
waitUntilFunctionUpdatedV2,
|
waitUntilFunctionUpdatedV2,
|
||||||
|
LogType,
|
||||||
} from '@aws-sdk/client-lambda';
|
} from '@aws-sdk/client-lambda';
|
||||||
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
|
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
@ -262,6 +263,21 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
await lambdaBuildDirectoryManager.clean();
|
await lambdaBuildDirectoryManager.clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractLogs(logString: string): string {
|
||||||
|
const formattedLogString = Buffer.from(logString, 'base64')
|
||||||
|
.toString('utf8')
|
||||||
|
.split('\t')
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return formattedLogString
|
||||||
|
.replace(/^(START|END|REPORT).*\n?/gm, '')
|
||||||
|
.replace(
|
||||||
|
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z) [a-f0-9-]+ INFO /gm,
|
||||||
|
'$1 INFO ',
|
||||||
|
)
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object,
|
payload: object,
|
||||||
@ -302,6 +318,7 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
const params: InvokeCommandInput = {
|
const params: InvokeCommandInput = {
|
||||||
FunctionName: serverlessFunction.id,
|
FunctionName: serverlessFunction.id,
|
||||||
Payload: JSON.stringify(executorPayload),
|
Payload: JSON.stringify(executorPayload),
|
||||||
|
LogType: LogType.Tail,
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = new InvokeCommand(params);
|
const command = new InvokeCommand(params);
|
||||||
@ -313,6 +330,8 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
? JSON.parse(result.Payload.transformToString())
|
? JSON.parse(result.Payload.transformToString())
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
const logs = result.LogResult ? this.extractLogs(result.LogResult) : '';
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
if (result.FunctionError) {
|
if (result.FunctionError) {
|
||||||
@ -321,11 +340,13 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
duration,
|
duration,
|
||||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
error: parsedResult,
|
error: parsedResult,
|
||||||
|
logs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: parsedResult,
|
data: parsedResult,
|
||||||
|
logs,
|
||||||
duration,
|
duration,
|
||||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
@ -129,6 +130,55 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
info: console.info,
|
||||||
|
debug: console.debug,
|
||||||
|
};
|
||||||
|
|
||||||
|
const interceptConsole = (
|
||||||
|
callback: (type: string, message: any[]) => void,
|
||||||
|
) => {
|
||||||
|
Object.keys(originalConsole).forEach((method) => {
|
||||||
|
console[method] = (...args: any[]) => {
|
||||||
|
callback(method, args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let logs = '';
|
||||||
|
|
||||||
|
interceptConsole((type, args) => {
|
||||||
|
const formattedArgs = args.map((arg) => {
|
||||||
|
if (typeof arg === 'object' && arg !== null) {
|
||||||
|
const seen = new WeakSet();
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
arg,
|
||||||
|
(key, value) => {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return '[Circular]'; // Handle circular references
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedType = type === 'log' ? 'info' : type;
|
||||||
|
|
||||||
|
logs += `${new Date().toISOString()} ${formattedType.toUpperCase()} ${formattedArgs.join(' ')}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mainFile = await import(compiledCodeFilePath);
|
const mainFile = await import(compiledCodeFilePath);
|
||||||
|
|
||||||
@ -141,12 +191,14 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: result,
|
data: result,
|
||||||
|
logs,
|
||||||
duration,
|
duration,
|
||||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
|
logs,
|
||||||
duration: Date.now() - startTime,
|
duration: Date.now() - startTime,
|
||||||
error: {
|
error: {
|
||||||
errorType: 'UnhandledError',
|
errorType: 'UnhandledError',
|
||||||
@ -156,6 +208,12 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
|
// Restoring originalConsole
|
||||||
|
Object.keys(originalConsole).forEach((method) => {
|
||||||
|
console[method] = (...args: any[]) => {
|
||||||
|
originalConsole[method](...args);
|
||||||
|
};
|
||||||
|
});
|
||||||
await fs.rm(compiledCodeFolderPath, { recursive: true, force: true });
|
await fs.rm(compiledCodeFolderPath, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,9 @@ export class ServerlessFunctionExecutionResultDTO {
|
|||||||
})
|
})
|
||||||
data?: JSON;
|
data?: JSON;
|
||||||
|
|
||||||
|
@Field({ description: 'Execution Logs' })
|
||||||
|
logs: string;
|
||||||
|
|
||||||
@Field({ description: 'Execution duration in milliseconds' })
|
@Field({ description: 'Execution duration in milliseconds' })
|
||||||
duration: number;
|
duration: number;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user