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:
2026-04-08 16:51:16 +03:00
parent af45a281dd
commit 7527a0b34b
3 changed files with 43 additions and 9 deletions
+31 -3
View File
@@ -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 });
+3 -2
View File
@@ -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}`);
}),
);
}
+9 -4
View File
@@ -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');
}