donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
218 lines • 9.73 kB
JavaScript
;
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