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 \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 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 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,
|
||||
@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -45,6 +45,7 @@ export const useTestServerlessFunction = ({
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
logs: result?.data?.executeOneServerlessFunction?.logs || '',
|
||||
duration: result?.data?.executeOneServerlessFunction?.duration,
|
||||
status: result?.data?.executeOneServerlessFunction?.status,
|
||||
error: result?.data?.executeOneServerlessFunction?.error
|
||||
|
||||
@ -6,6 +6,7 @@ export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
) {
|
||||
executeOneServerlessFunction(input: $input) {
|
||||
data
|
||||
logs
|
||||
duration
|
||||
status
|
||||
error
|
||||
|
||||
@ -14,6 +14,7 @@ export type TextAreaProps = {
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
@ -73,12 +74,13 @@ export const TextArea = ({
|
||||
disabled,
|
||||
placeholder,
|
||||
minRows = 1,
|
||||
maxRows = MAX_ROWS,
|
||||
value = '',
|
||||
className,
|
||||
onChange,
|
||||
onBlur,
|
||||
}: TextAreaProps) => {
|
||||
const computedMinRows = Math.min(minRows, MAX_ROWS);
|
||||
const computedMinRows = Math.min(minRows, maxRows);
|
||||
|
||||
const inputId = useId();
|
||||
|
||||
@ -103,7 +105,7 @@ export const TextArea = ({
|
||||
<StyledTextArea
|
||||
id={inputId}
|
||||
placeholder={placeholder}
|
||||
maxRows={MAX_ROWS}
|
||||
maxRows={maxRows}
|
||||
minRows={computedMinRows}
|
||||
value={value}
|
||||
onChange={(event) =>
|
||||
|
||||
@ -5,6 +5,7 @@ export type ServerlessFunctionTestData = {
|
||||
input: { [field: string]: any };
|
||||
output: {
|
||||
data?: string;
|
||||
logs: string;
|
||||
duration?: number;
|
||||
status?: ServerlessFunctionExecutionStatus;
|
||||
error?: string;
|
||||
@ -15,6 +16,7 @@ export type ServerlessFunctionTestData = {
|
||||
|
||||
export const DEFAULT_OUTPUT_VALUE = {
|
||||
data: 'Enter an input above then press "Test"',
|
||||
logs: '',
|
||||
status: ServerlessFunctionExecutionStatus.IDLE,
|
||||
};
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { CodeEditor, IconCode, IconPlayerPlay, useIcons } from 'twenty-ui';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -340,6 +341,16 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
||||
isTesting={isTesting}
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
<StyledCodeEditorContainer>
|
||||
<InputLabel>Logs</InputLabel>
|
||||
<TextArea
|
||||
value={
|
||||
isTesting ? '' : serverlessFunctionTestData.output.logs
|
||||
}
|
||||
maxRows={20}
|
||||
disabled
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
</>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
|
||||
@ -10,6 +10,7 @@ export type ServerlessExecuteError = {
|
||||
export type ServerlessExecuteResult = {
|
||||
data: object | null;
|
||||
duration: number;
|
||||
logs: string;
|
||||
status: ServerlessFunctionExecutionStatus;
|
||||
error?: ServerlessExecuteError;
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
PublishLayerVersionCommandInput,
|
||||
ResourceNotFoundException,
|
||||
waitUntilFunctionUpdatedV2,
|
||||
LogType,
|
||||
} from '@aws-sdk/client-lambda';
|
||||
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
@ -262,6 +263,21 @@ export class LambdaDriver implements ServerlessDriver {
|
||||
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(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object,
|
||||
@ -302,6 +318,7 @@ export class LambdaDriver implements ServerlessDriver {
|
||||
const params: InvokeCommandInput = {
|
||||
FunctionName: serverlessFunction.id,
|
||||
Payload: JSON.stringify(executorPayload),
|
||||
LogType: LogType.Tail,
|
||||
};
|
||||
|
||||
const command = new InvokeCommand(params);
|
||||
@ -313,6 +330,8 @@ export class LambdaDriver implements ServerlessDriver {
|
||||
? JSON.parse(result.Payload.transformToString())
|
||||
: {};
|
||||
|
||||
const logs = result.LogResult ? this.extractLogs(result.LogResult) : '';
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.FunctionError) {
|
||||
@ -321,11 +340,13 @@ export class LambdaDriver implements ServerlessDriver {
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: parsedResult,
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: parsedResult,
|
||||
logs,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import { promises as fs } from 'fs';
|
||||
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 {
|
||||
const mainFile = await import(compiledCodeFilePath);
|
||||
|
||||
@ -141,12 +191,14 @@ export class LocalDriver implements ServerlessDriver {
|
||||
|
||||
return {
|
||||
data: result,
|
||||
logs,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
logs,
|
||||
duration: Date.now() - startTime,
|
||||
error: {
|
||||
errorType: 'UnhandledError',
|
||||
@ -156,6 +208,12 @@ export class LocalDriver implements ServerlessDriver {
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
};
|
||||
} finally {
|
||||
// Restoring originalConsole
|
||||
Object.keys(originalConsole).forEach((method) => {
|
||||
console[method] = (...args: any[]) => {
|
||||
originalConsole[method](...args);
|
||||
};
|
||||
});
|
||||
await fs.rm(compiledCodeFolderPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,9 @@ export class ServerlessFunctionExecutionResultDTO {
|
||||
})
|
||||
data?: JSON;
|
||||
|
||||
@Field({ description: 'Execution Logs' })
|
||||
logs: string;
|
||||
|
||||
@Field({ description: 'Execution duration in milliseconds' })
|
||||
duration: number;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user