UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

218 lines 9.73 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.networkLogger = exports.browserLogger = exports.accessLogger = exports.appLogger = exports.loggingContext = void 0; exports.setProcessLocalFlowId = setProcessLocalFlowId; exports.setProcessLocalLogBuffer = setProcessLocalLogBuffer; exports.logErrorWithoutStack = logErrorWithoutStack; exports.formatLogInfoForTest = formatLogInfoForTest; const async_hooks_1 = require("async_hooks"); const path_1 = __importDefault(require("path")); const winston_1 = __importDefault(require("winston")); const winston_transport_1 = __importDefault(require("winston-transport")); const envVars_1 = require("../envVars"); const MiscUtils_1 = require("../utils/MiscUtils"); // Private constants and utility functions const LOG_MAX_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB cap per file const MAX_FILES = 2; /** * Formats an error object into a detailed string representation */ function formatError(error) { return `${MiscUtils_1.MiscUtils.errName(error)}: ${error.message}\n${error.stack || ''}`; } /** * Winston stores extra positional args (e.g., logger.error('msg', err, meta)) * on this symbol when no `format.splat()` is used. We pull from it so that * callers who pass Errors or other values as additional arguments still get * those details rendered, even though we do our own formatting. */ const SPLAT = Symbol.for('splat'); /** * Extracts an Error instance that winston may have tucked into the splat array * or the `error` property when logging like `logger.error('msg', error)`. */ function getErrorFromInfo(info) { if (info instanceof Error) { return info; } if (info?.error instanceof Error) { return info.error; } const splat = info?.[SPLAT]; if (Array.isArray(splat)) { const errorFromSplat = splat.find((item) => item instanceof Error); if (errorFromSplat) { return errorFromSplat; } } return null; } // Configure common logging settings const basePath = MiscUtils_1.MiscUtils.baseWorkingDirectory(); const timestampFormat = 'YYYY-MM-DD HH:mm:ss'; // Custom format that handles errors properly const errorAwareFormat = winston_1.default.format((info) => { const error = getErrorFromInfo(info); if (!error) { return info; } const messagePrefix = info instanceof Error || !info?.message ? '' : `${info.message}\n`; return { ...info, message: `${messagePrefix}${formatError(error)}`, stack: error.stack, name: error.constructor.name, }; }); exports.loggingContext = new async_hooks_1.AsyncLocalStorage(); /** * Custom Winston transport that routes log entries into the per-flow * {@link FlowLogBuffer} stored in the {@link loggingContext} async local * storage. Each logger instance creates its own transport tagged with the * appropriate source so the buffer knows the origin. */ class FlowLogBufferTransport extends winston_transport_1.default { constructor(source) { super(); this.source = source; } log(info, callback) { const store = exports.loggingContext.getStore(); const buffer = store?.logBuffer || processLocalLogBuffer; if (buffer) { buffer.push({ ...info, source: this.source }); } callback(); } } let processLocalFlowId = null; let processLocalLogBuffer = null; /** * ################################################################################### * # WARNING! Only use this function within the context of Playwright test fixtures! # * ################################################################################### * * Playwright spins up one worker process per shard and constructs the entire * fixture graph (including the user test function) before entering our * AsyncLocalStorage scopes. That means early logs in the test body may execute * outside the ALS store even though we seed it later in the fixture. * * We therefore keep a *process-local* fallback that stores the most recent flow * ID and log buffer for the worker. Because Playwright guarantees only one test * runs at a time per process, this is safe: there is no race between concurrent * tests in the same worker, yet we still avoid leaking IDs across workers. */ function setProcessLocalFlowId(flowId) { processLocalFlowId = flowId; } function setProcessLocalLogBuffer(buffer) { processLocalLogBuffer = buffer; } // Format to add the currently running flow's ID (if any) from AsyncLocalStorage const flowIdFormat = winston_1.default.format((info) => { const store = exports.loggingContext.getStore(); // We prefer the ALS-provided ID but fall back to the process-local slot for // logs emitted before the ALS store is available (see note above). const flowId = store?.flowId || processLocalFlowId || '-'; return { ...info, flowId, }; }); // Create and export the application logger directly exports.appLogger = winston_1.default.createLogger({ level: envVars_1.env.data.LOG_LEVEL, format: winston_1.default.format.combine(errorAwareFormat(), flowIdFormat(), winston_1.default.format.timestamp({ format: timestampFormat }), winston_1.default.format.json()), transports: [ // Console transport (stderr) new winston_1.default.transports.Console({ stderrLevels: ['info', 'warn', 'error'], format: winston_1.default.format.combine(winston_1.default.format.timestamp({ format: 'HH:mm:ss.SSS' }), flowIdFormat(), winston_1.default.format.printf(({ timestamp, level, message, flowId, stack }) => `${timestamp} [${flowId}] ${level.toUpperCase().padEnd(5)} ${message}${stack ? '\n' + stack : ''}`)), }), // File transport with size‑based rotation (1 MB per file, keep 2 files) new winston_1.default.transports.File({ filename: path_1.default.join(basePath, 'app.log'), maxsize: LOG_MAX_SIZE_BYTES, maxFiles: MAX_FILES, tailable: true, }), // Per-flow ring buffer transport new FlowLogBufferTransport('donobu'), ], }); // Create and export the access logger directly exports.accessLogger = winston_1.default.createLogger({ level: 'info', format: winston_1.default.format.combine(winston_1.default.format.timestamp({ format: timestampFormat }), winston_1.default.format.printf(({ timestamp, message }) => `${timestamp} access - ${message}`)), transports: [ // Console transport (stderr) new winston_1.default.transports.Console({ stderrLevels: ['info', 'warn', 'error'], format: winston_1.default.format.combine(winston_1.default.format.timestamp({ format: 'HH:mm:ss.SSS' }), winston_1.default.format.printf(({ timestamp, message }) => `${timestamp} access - ${message}`)), }), // File transport with size‑based rotation (1 MB per file, keep 2 files) new winston_1.default.transports.File({ filename: path_1.default.join(basePath, 'access.log'), maxsize: LOG_MAX_SIZE_BYTES, maxFiles: MAX_FILES, tailable: true, }), ], }); // Create and export the browser console logger. Note that this is not being // forwarded to stderr since browser logs can be very noisy. exports.browserLogger = winston_1.default.createLogger({ level: envVars_1.env.data.LOG_LEVEL, format: winston_1.default.format.combine(flowIdFormat(), winston_1.default.format.timestamp({ format: timestampFormat }), winston_1.default.format.json()), transports: [ // File transport with size‑based rotation (1 MB per file, keep 2 files) new winston_1.default.transports.File({ filename: path_1.default.join(basePath, 'browser.log'), maxsize: LOG_MAX_SIZE_BYTES, maxFiles: MAX_FILES, tailable: true, }), new FlowLogBufferTransport('browser'), ], }); // Create and export the network request logger. Like the browser logger, this // is file-only to avoid flooding stderr with per-request noise. exports.networkLogger = winston_1.default.createLogger({ level: envVars_1.env.data.LOG_LEVEL, format: winston_1.default.format.combine(flowIdFormat(), winston_1.default.format.timestamp({ format: timestampFormat }), winston_1.default.format.json()), transports: [ new winston_1.default.transports.File({ filename: path_1.default.join(basePath, 'network.log'), maxsize: LOG_MAX_SIZE_BYTES, maxFiles: MAX_FILES, tailable: true, }), new FlowLogBufferTransport('network'), ], }); function logErrorWithoutStack(message, error, level = 'error') { if (!error) { exports.appLogger.log({ level, message }); return; } const errorName = typeof error === 'object' && error !== null ? error.name || error.constructor?.name || 'Error' : 'Error'; const errorMessage = typeof error === 'string' ? error : typeof error?.message === 'string' ? error.message : String(error); const composedMessage = message ? `${message} - ${errorName}: ${errorMessage}` : `${errorName}: ${errorMessage}`; exports.appLogger.log({ level, message: composedMessage, errorName, errorMessage }); } // Test helper to exercise the error-aware formatting logic without invoking transports. function formatLogInfoForTest(info) { return errorAwareFormat().transform({ ...info }); } //# sourceMappingURL=Logger.js.map