logger.ts•4.21 kB
/**
* Logger utility for the Quickbase connector
*/
/**
* Log levels
*/
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3
}
/**
* Logger interface
*/
export interface Logger {
error(message: string, data?: unknown): void;
warn(message: string, data?: unknown): void;
info(message: string, data?: unknown): void;
debug(message: string, data?: unknown): void;
}
// Global log level (can be set via environment variable)
let globalLogLevel = process.env.LOG_LEVEL
? (LogLevel[process.env.LOG_LEVEL.toUpperCase() as keyof typeof LogLevel] || LogLevel.INFO)
: LogLevel.INFO;
/**
* Set the global log level
* @param level New log level
*/
export function setLogLevel(level: LogLevel | string): void {
if (typeof level === 'string') {
globalLogLevel = LogLevel[level.toUpperCase() as keyof typeof LogLevel] || LogLevel.INFO;
} else {
globalLogLevel = level;
}
}
/**
* Get the current global log level
* @returns Current log level
*/
export function getLogLevel(): string {
return LogLevel[globalLogLevel] || 'INFO';
}
/**
* Creates a logger with the specified name
* @param name Logger name
* @returns Logger instance
*/
export function createLogger(name: string): Logger {
const formatData = (data: unknown): string => {
if (data === undefined) return '';
try {
return JSON.stringify(redactSensitiveData(data));
} catch (error) {
// Safe error message formatting to prevent nested serialization issues
const errorMessage = error instanceof Error ? error.message : String(error);
return `[Unserializable data: ${errorMessage}]`;
}
};
return {
error(message: string, data?: unknown): void {
if (globalLogLevel >= LogLevel.ERROR) {
// Use stderr to avoid interfering with JSON responses on stdout
process.stderr.write(`[ERROR] ${name}: ${message} ${data ? formatData(data) : ''}\n`);
}
},
warn(message: string, data?: unknown): void {
if (globalLogLevel >= LogLevel.WARN) {
// Use stderr to avoid interfering with JSON responses on stdout
process.stderr.write(`[WARN] ${name}: ${message} ${data ? formatData(data) : ''}\n`);
}
},
info(message: string, data?: unknown): void {
if (globalLogLevel >= LogLevel.INFO) {
// Use stderr to avoid interfering with JSON responses on stdout
process.stderr.write(`[INFO] ${name}: ${message} ${data ? formatData(data) : ''}\n`);
}
},
debug(message: string, data?: unknown): void {
if (globalLogLevel >= LogLevel.DEBUG) {
// Use stderr to avoid interfering with JSON responses on stdout
process.stderr.write(`[DEBUG] ${name}: ${message} ${data ? formatData(data) : ''}\n`);
}
}
};
}
/**
* Sensitive keys that should be redacted
*/
const SENSITIVE_KEYS = [
'token',
'password',
'secret',
'auth',
'key',
'credential',
'Authorization',
'QB-USER-TOKEN',
'userToken',
'QUICKBASE_USER_TOKEN'
];
/**
* Redacts sensitive data in objects with circular reference protection
* @param data Object to redact
* @returns Redacted object
*/
function redactSensitiveData(data: unknown): unknown {
const visited = new WeakSet();
function redactRecursive(obj: unknown): unknown {
if (!obj || typeof obj !== 'object') {
return obj;
}
// Circular reference protection
if (visited.has(obj)) {
return '[Circular Reference]';
}
visited.add(obj);
if (Array.isArray(obj)) {
return obj.map(item => redactRecursive(item));
}
const result: Record<string, unknown> = {};
try {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
result[key] = redactRecursive(value);
} else if (
typeof value === 'string' &&
SENSITIVE_KEYS.some(k => key.toLowerCase().includes(k.toLowerCase()))
) {
result[key] = '***REDACTED***';
} else {
result[key] = value;
}
}
} catch (error) {
return '[Unserializable Object]';
}
return result;
}
return redactRecursive(data);
}