feat(logging): redirect script console output through service logger
- suppress raw stdout from exec/validate code by overriding console - route console.log/warn/error/info through ScriptLogger callback - expose helpers.log/warn/error for scripts to use explicitly - scheduler passes step-run-prefixed logger to all execute() calls - log response body length instead of full body in LoggingInterceptor
This commit is contained in:
@@ -8,6 +8,8 @@ export interface ExecResult {
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
export type ScriptLogger = (level: 'log' | 'warn' | 'error', message: string) => void;
|
||||
|
||||
@Injectable()
|
||||
export class CodeExecutorService {
|
||||
private readonly logger = new TraceLogger(CodeExecutorService.name);
|
||||
@@ -29,13 +31,39 @@ export class CodeExecutorService {
|
||||
* Executes `code` as an async function body with `page` and `context` in
|
||||
* scope. Always call `validate()` before this method.
|
||||
*/
|
||||
async execute(page: Page, context: BrowserContext, code: string): Promise<ExecResult> {
|
||||
async execute(page: Page, context: BrowserContext, code: string, log?: ScriptLogger): Promise<ExecResult> {
|
||||
const scriptLog: ScriptLogger = log ?? ((level, msg) => this.logger[level](msg));
|
||||
const toStr = (args: unknown[]) => args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
|
||||
|
||||
try {
|
||||
const pageHelpers = { dumpDom: (selector?: string) => dumpDom(page, selector) };
|
||||
const pageHelpers = {
|
||||
dumpDom: (selector?: string) => dumpDom(page, selector),
|
||||
log: (...args: unknown[]) => scriptLog('log', toStr(args)),
|
||||
warn: (...args: unknown[]) => scriptLog('warn', toStr(args)),
|
||||
error: (...args: unknown[]) => scriptLog('error', toStr(args)),
|
||||
};
|
||||
|
||||
const prevLog = console.log;
|
||||
const prevWarn = console.warn;
|
||||
const prevError = console.error;
|
||||
const prevInfo = console.info;
|
||||
console.log = (...args: unknown[]) => scriptLog('log', toStr(args));
|
||||
console.warn = (...args: unknown[]) => scriptLog('warn', toStr(args));
|
||||
console.error = (...args: unknown[]) => scriptLog('error', toStr(args));
|
||||
console.info = (...args: unknown[]) => scriptLog('log', toStr(args));
|
||||
|
||||
// eslint-disable-next-line no-new-func
|
||||
const fn = new Function('page', 'context', 'helpers', `return (async (page, context, helpers) => { ${code} })(page, context, helpers)`);
|
||||
this.logger.debug('Executing user code');
|
||||
const result = await fn(page, context, pageHelpers);
|
||||
let result: unknown;
|
||||
try {
|
||||
result = await fn(page, context, pageHelpers);
|
||||
} finally {
|
||||
console.log = prevLog;
|
||||
console.warn = prevWarn;
|
||||
console.error = prevError;
|
||||
console.info = prevInfo;
|
||||
}
|
||||
return { result };
|
||||
} catch (err) {
|
||||
throw new InternalServerErrorException(`Code execution failed: ${(err as Error).message}`, { cause: err });
|
||||
|
||||
@@ -25,8 +25,9 @@ export class LoggingInterceptor implements NestInterceptor {
|
||||
return next.handle().pipe(
|
||||
tap((responseBody) => {
|
||||
const ms = Date.now() - start;
|
||||
const resStr = responseBody != null ? ` ${JSON.stringify(responseBody)}` : '';
|
||||
this.logger.debug(`← ${method} ${url} ${res.statusCode} (${ms}ms)${resStr}`);
|
||||
const len = responseBody != null ? JSON.stringify(responseBody).length : 0;
|
||||
const lenStr = len > 0 ? ` [${len}b]` : '';
|
||||
this.logger.debug(`← ${method} ${url} ${res.statusCode} (${ms}ms)${lenStr}`);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Browser, BrowserContext, Page } from 'playwright';
|
||||
import { ScenarioRunEntity } from './scenario-run.entity';
|
||||
import { ScenarioRunStepEntity } from './scenario-run-step.entity';
|
||||
import { ScenarioStepEntity } from './scenario-step.entity';
|
||||
import type { ScriptLogger } from '../code-executor/code-executor.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { CodeExecutorService } from '../code-executor/code-executor.service';
|
||||
import { SessionService } from '../session/session.service';
|
||||
@@ -39,6 +40,10 @@ export class ScenarioSchedulerService {
|
||||
private readonly sessionService: SessionService,
|
||||
) {}
|
||||
|
||||
private stepLogger(stepRunId: number): ScriptLogger {
|
||||
return (level, msg) => this.logger[level](`StepRun #${stepRunId} script: ${msg}`);
|
||||
}
|
||||
|
||||
// ── Job 1: flip pending runs → in_progress ─────────────────────────────────
|
||||
|
||||
@Interval(1000)
|
||||
@@ -157,7 +162,7 @@ export class ScenarioSchedulerService {
|
||||
if (step.validateCode) {
|
||||
const { page, context } = await this.getOrCreateBrowserHandle(stepRun.runId, step.sessionName);
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(page, context, step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(page, context, step.validateCode, this.stepLogger(stepRun.id));
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? 'Validation failed');
|
||||
}
|
||||
@@ -172,12 +177,12 @@ export class ScenarioSchedulerService {
|
||||
|
||||
this.codeExecutor.validate(step.execCode);
|
||||
const { page, context } = await this.getOrCreateBrowserHandle(stepRun.runId, step.sessionName);
|
||||
await this.codeExecutor.execute(page, context, step.execCode);
|
||||
await this.codeExecutor.execute(page, context, step.execCode, this.stepLogger(stepRun.id));
|
||||
this.logger.log(`StepRun #${stepRun.id}: exec OK`);
|
||||
|
||||
if (step.validateCode) {
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(page, context, step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(page, context, step.validateCode, this.stepLogger(stepRun.id));
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? 'Validation failed');
|
||||
}
|
||||
@@ -203,7 +208,7 @@ export class ScenarioSchedulerService {
|
||||
|
||||
if (step.validateCode) {
|
||||
this.codeExecutor.validate(step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(page, context, step.validateCode);
|
||||
const { result } = await this.codeExecutor.execute(page, context, step.validateCode, this.stepLogger(stepRun.id));
|
||||
const vr = this.parseValidateResult(result);
|
||||
if (!vr.success) throw new Error(vr.description ?? 'Validation failed');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user