UNPKG

@fjell/logging

Version:
731 lines (718 loc) 21.3 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/LogFormat.ts var LogFormat_exports = {}; __export(LogFormat_exports, { LogFormats: () => LogFormats, STRUCTURED: () => STRUCTURED, TEXT: () => TEXT, getConfig: () => getConfig }); var TEXT = { name: "TEXT", description: "Text format" }; var STRUCTURED = { name: "STRUCTURED", description: "Structured format" }; var LogFormats = [ TEXT, STRUCTURED ]; var getConfig = (name) => { const config = LogFormats.find((config2) => config2.name === name); if (!config) { throw new Error(`Invalid Log Format Supplied to Logging Configuration '${name}'`); } return config; }; // src/LogLevel.ts var LogLevel_exports = {}; __export(LogLevel_exports, { ALERT: () => ALERT, CRITICAL: () => CRITICAL, DEBUG: () => DEBUG, DEFAULT: () => DEFAULT, EMERGENCY: () => EMERGENCY, ERROR: () => ERROR, INFO: () => INFO, LogLevels: () => LogLevels, NOTICE: () => NOTICE, TRACE: () => TRACE, WARNING: () => WARNING, getConfig: () => getConfig2 }); var EMERGENCY = { name: "EMERGENCY", value: 0 }; var ALERT = { name: "ALERT", value: 1 }; var CRITICAL = { name: "CRITICAL", value: 2 }; var ERROR = { name: "ERROR", value: 3 }; var WARNING = { name: "WARNING", value: 4 }; var NOTICE = { name: "NOTICE", value: 5 }; var INFO = { name: "INFO", value: 6 }; var DEBUG = { name: "DEBUG", value: 7 }; var TRACE = { name: "TRACE", value: 8 }; var DEFAULT = { name: "DEFAULT", value: 9 }; var LogLevels = [ EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, TRACE, DEFAULT ]; var getConfig2 = (name) => { const config = LogLevels.find((config2) => config2.name === name); if (!config) { throw new Error(`Invalid Log Level Supplied to Logging Configuration '${name}'`); } return config; }; // src/utils/maskSensitive.ts var PRIVATE_KEY_PATTERNS = [ /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----\s*[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/gi, /-----BEGIN\s+EC\s+PRIVATE\s+KEY-----\s*[\s\S]*?-----END\s+EC\s+PRIVATE\s+KEY-----/gi, /-----BEGIN\s+DSA\s+PRIVATE\s+KEY-----\s*[\s\S]*?-----END\s+DSA\s+PRIVATE\s+KEY-----/gi ]; var BASE64_BLOB_PATTERN = /[A-Za-z0-9+/=]{200,}/g; var JWT_PATTERN = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/; var EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g; var SSN_PATTERN = /\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b/g; function maskString(input) { if (typeof input !== "string" || input.length === 0) { return input; } let masked = input; for (const pattern of PRIVATE_KEY_PATTERNS) { masked = masked.replace(pattern, "****"); } masked = masked.replace(BASE64_BLOB_PATTERN, "****"); if (JWT_PATTERN.test(masked)) { const segments = masked.split("."); if (segments.length === 3) { const [, middle, third] = segments; if (middle.length > 100 || third.length > 100) { masked = "****"; } } } masked = masked.replace(EMAIL_PATTERN, "****"); masked = masked.replace(SSN_PATTERN, "****"); return masked; } function maskObject(obj, maxDepth = 8, currentDepth = 0) { if (currentDepth >= maxDepth) { return obj; } if (obj === null || obj === void 0) { return obj; } if (typeof obj === "string") { return maskString(obj); } if (typeof obj !== "object") { return obj; } if (Array.isArray(obj)) { return obj.map((item) => maskObject(item, maxDepth, currentDepth + 1)); } const maskedObj = {}; for (const [key, value] of Object.entries(obj)) { if (Object.prototype.hasOwnProperty.call(obj, key)) { maskedObj[key] = maskObject(value, maxDepth, currentDepth + 1); } } return maskedObj; } var defaultMaskingConfig = { enabled: false, maskEmails: true, maskSSNs: true, maskPrivateKeys: true, maskBase64Blobs: true, maskJWTs: true, maxDepth: 8 }; function maskWithConfig(input, config = defaultMaskingConfig) { if (!config.enabled) { return input; } const customMaskString = (str) => { if (typeof str !== "string" || str.length === 0) { return str; } let masked = str; if (config.maskPrivateKeys) { for (const pattern of PRIVATE_KEY_PATTERNS) { masked = masked.replace(pattern, "****"); } } if (config.maskBase64Blobs) { masked = masked.replace(BASE64_BLOB_PATTERN, "****"); } if (config.maskJWTs && JWT_PATTERN.test(masked)) { const segments = masked.split("."); if (segments.length === 3) { const [, middle, third] = segments; if (middle.length > 100 || third.length > 100) { masked = "****"; } } } if (config.maskEmails) { masked = masked.replace(EMAIL_PATTERN, "****"); } if (config.maskSSNs) { masked = masked.replace(SSN_PATTERN, "****"); } return masked; }; const customMaskObject = (obj, maxDepth = config.maxDepth, currentDepth = 0) => { if (currentDepth >= maxDepth) { return obj; } if (obj === null || obj === void 0) { return obj; } if (typeof obj === "string") { return customMaskString(obj); } if (typeof obj !== "object") { return obj; } if (Array.isArray(obj)) { return obj.map((item) => customMaskObject(item, maxDepth, currentDepth + 1)); } const maskedObj = {}; for (const [key, value] of Object.entries(obj)) { if (Object.prototype.hasOwnProperty.call(obj, key)) { maskedObj[key] = customMaskObject(value, maxDepth, currentDepth + 1); } } return maskedObj; }; return customMaskObject(input); } // src/config.ts var defaultLogLevel = INFO; var defaultLogFormat = TEXT; var defaultLoggingConfig = { logLevel: defaultLogLevel, logFormat: defaultLogFormat, overrides: {}, floodControl: { enabled: false, threshold: 10, timeframe: 1e3 // 1 second }, masking: defaultMaskingConfig }; var convertOverrides = (overrides) => { const convertedOverrides = {}; if (overrides) { Object.entries(overrides).forEach(([key, value]) => { convertedOverrides[key] = { logLevel: value.logLevel ? getConfig2(value.logLevel) : defaultLogLevel }; }); } return convertedOverrides; }; var convertConfig = (config) => { return { logLevel: config.logLevel ? getConfig2(config.logLevel) : defaultLogLevel, logFormat: config.logFormat ? getConfig(config.logFormat) : defaultLogFormat, overrides: convertOverrides(config.overrides), floodControl: { ...defaultLoggingConfig.floodControl, ...config.floodControl || {} }, masking: { ...defaultLoggingConfig.masking, ...config.masking || {} } }; }; var configureLogging = () => { let config = {}; const loggingConfigEnv = process.env.LOGGING_CONFIG; const expoLoggingConfigEnv = process.env.EXPO_PUBLIC_LOGGING_CONFIG; const nextLoggingConfigEnv = process.env.NEXT_PUBLIC_LOGGING_CONFIG; let logLevelEnv = process.env.LOG_LEVEL; let logFormatEnv = process.env.LOG_FORMAT; if (loggingConfigEnv) { try { config = JSON.parse(loggingConfigEnv); } catch (error) { console.error("Invalid JSON in LOGGING_CONFIG environment variable:", error); config = {}; } } else if (expoLoggingConfigEnv) { try { config = JSON.parse(expoLoggingConfigEnv); } catch (error) { console.error("Invalid JSON in EXPO_PUBLIC_LOGGING_CONFIG environment variable:", error); config = {}; } } else if (nextLoggingConfigEnv) { try { config = JSON.parse(nextLoggingConfigEnv); } catch (error) { console.error("Invalid JSON in NEXT_PUBLIC_LOGGING_CONFIG environment variable:", error); config = {}; } } const convertedConfig = convertConfig(config); if (logLevelEnv) { logLevelEnv = logLevelEnv?.toUpperCase(); const logLevelConfig = getConfig2(logLevelEnv); convertedConfig.logLevel = logLevelConfig; } if (logFormatEnv) { logFormatEnv = logFormatEnv.toUpperCase(); const logFormatConfig = getConfig(logFormatEnv); convertedConfig.logFormat = logFormatConfig; } const finalConfig = { ...defaultLoggingConfig, ...convertedConfig }; return finalConfig; }; // src/Writer.ts var createWriter = (formatter, logMethod, options = {}) => { const { respectInjectedMethod = false, errorMethod = console.error, warningMethod = console.warn, infoMethod = console.log } = options; return { write: (level, coordinates, payload) => { let finalLogMethod = logMethod; if (!respectInjectedMethod) { if (level.name === ERROR.name || level.name === CRITICAL.name || level.name === ALERT.name || level.name === EMERGENCY.name) { finalLogMethod = errorMethod; } else if (level.name === WARNING.name) { finalLogMethod = warningMethod; } else { finalLogMethod = infoMethod; } } finalLogMethod(formatter.formatLog(level, coordinates, payload)); } }; }; // src/utils.ts var stringifyJSON = function(obj, visited = /* @__PURE__ */ new Set()) { const arrOfKeyVals = []; const arrVals = []; let objKeys = []; if (typeof obj === "number" || typeof obj === "boolean" || obj === null) return "" + obj; else if (typeof obj === "string") return '"' + obj + '"'; if (obj instanceof Object && visited.has(obj)) { return '"(circular)"'; } else if (Array.isArray(obj)) { if (obj.length === 0) return "[]"; else { visited.add(obj); obj.forEach(function(el) { arrVals.push(stringifyJSON(el, visited)); }); visited.delete(obj); return "[" + arrVals + "]"; } } else if (obj instanceof Object) { visited.add(obj); objKeys = Object.keys(obj); objKeys.forEach(function(key) { const keyOut = '"' + key + '":'; const keyValOut = obj[key]; if (keyValOut instanceof Function || typeof keyValOut === "undefined") return; else if (typeof keyValOut === "string") arrOfKeyVals.push(keyOut + '"' + keyValOut + '"'); else if (typeof keyValOut === "boolean" || typeof keyValOut === "number" || keyValOut === null) arrOfKeyVals.push(keyOut + keyValOut); else if (keyValOut instanceof Object) { arrOfKeyVals.push(keyOut + stringifyJSON(keyValOut, visited)); } }); visited.delete(obj); return "{" + arrOfKeyVals + "}"; } return ""; }; var safeFormat = (message, ...args) => { let result = message; let argIndex = 0; result = result.replace(/%([sdjifoO%])/g, (match, specifier) => { if (specifier === "%") { return "%"; } if (argIndex >= args.length) { return match; } const arg = args[argIndex++]; switch (specifier) { case "s": return String(arg); case "d": return String(parseInt(arg, 10)); case "i": return String(parseInt(arg, 10)); case "f": return String(parseFloat(arg)); case "j": try { return stringifyJSON(arg); } catch { return String(arg); } case "o": return stringifyJSON(arg); case "O": return stringifyJSON(arg); default: return String(arg); } }); return result; }; var safeInspect = (obj) => { try { return stringifyJSON(obj); } catch { return `[Object: ${typeof obj}]`; } }; // src/formatter.ts var createFormatter = (logFormat) => { if (logFormat.name === "TEXT") { return getTextFormatter(); } else if (logFormat.name === "STRUCTURED") { return getStructuredFormatter(); } throw new Error(`Unknown log format: ${logFormat.name}`); }; var getTextFormatter = () => { const formatLog = (level, coordinates, payload) => { const hasSpecifiers = /%[sdjifoO%]/.test(payload.message); let logMessage; if (payload.data.length === 0) { logMessage = payload.message; } else if (hasSpecifiers) { logMessage = safeFormat(payload.message, ...payload.data); } else { logMessage = `${payload.message} ${safeInspect(payload.data)}`; } return `(${(/* @__PURE__ */ new Date()).valueOf()}) [${level.name}] - [${coordinates.category}] ${coordinates.components.map((c) => `[${c}]`)} ${logMessage}`; }; const timerMessage = (level, coordinates, payload) => { const randomInt = Math.floor(Math.random() * 1e6); const timerMessage2 = `(${(/* @__PURE__ */ new Date()).valueOf()}) [${level.name}] - [${coordinates.category}] ${coordinates.components.map((c) => `[${c}]`)} ${safeFormat(payload.message, ...payload.data)} ${safeInspect(payload.data)} ${randomInt}`; return timerMessage2; }; return { formatLog, timerMessage, getLogFormat: () => TEXT }; }; var getStructuredFormatter = () => { const formatLog = (level, coordinates, payload) => { const severity = level.name; const hasSpecifiers = /%[sdjifoO%]/.test(payload.message); return JSON.stringify({ severity, message: hasSpecifiers ? safeFormat(payload.message, ...payload.data) : payload.message, "logging.googleapis.com/labels": { category: coordinates.category, components: `${coordinates.components.map((c) => `[${c}]`)}` }, ...!hasSpecifiers && payload.data.length > 0 && { data: safeInspect(payload.data) } }); }; const timerMessage = (level, coordinates, payload) => { const severity = level.name; const randomInt = Math.floor(Math.random() * 1e6); return JSON.stringify({ severity, message: safeFormat(payload.message, ...payload.data), "logging.googleapis.com/labels": { category: coordinates.category, components: `${coordinates.components.map((c) => `[${c}]`)}` }, data: safeInspect(payload.data), "logging.googleapis.com/spanId": String(randomInt) }); }; return { formatLog, timerMessage, getLogFormat: () => STRUCTURED }; }; // src/FloodControl.ts var hash = (message, data) => { const dataString = data.map((item) => { try { return JSON.stringify(item); } catch { return stringifyJSON(item); } }).join(""); return `${message}${dataString}`; }; var FloodControl = class { config; history = /* @__PURE__ */ new Map(); suppressed = /* @__PURE__ */ new Map(); cleanupTimer = null; constructor(config) { this.config = config; if (this.config.enabled) { this.cleanupTimer = setInterval(() => this.cleanup(), this.config.timeframe * 2); } } destroy() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } } cleanup() { const now = Date.now(); for (const [hash2, timestamps] of this.history.entries()) { const recentTimestamps = timestamps.filter( (timestamp) => now - timestamp < this.config.timeframe ); if (recentTimestamps.length > 0) { this.history.set(hash2, recentTimestamps); } else { this.history.delete(hash2); this.suppressed.delete(hash2); } } } check(message, data) { if (!this.config.enabled) { return "log"; } const messageHash = hash(message, data); const now = Date.now(); const timestamps = (this.history.get(messageHash) || []).filter( (timestamp) => now - timestamp < this.config.timeframe ); timestamps.push(now); this.history.set(messageHash, timestamps); if (timestamps.length > this.config.threshold) { const suppressedInfo = this.suppressed.get(messageHash); if (suppressedInfo) { suppressedInfo.count++; return "suppress"; } else { this.suppressed.set(messageHash, { count: 1, firstTimestamp: timestamps[0], summaryLogged: false }); return "suppress"; } } else { if (this.suppressed.has(messageHash)) { this.suppressed.delete(messageHash); return "resume"; } } return "log"; } getSuppressedCount(message, data) { const messageHash = hash(message, data); return this.suppressed.get(messageHash)?.count || 0; } }; // src/Logger.ts var createLogger = (logFormat, logLevel, coordinates, floodControlConfig, writerOptions) => { const formatter = createFormatter(logFormat); const floodControl = floodControlConfig.enabled ? new FloodControl(floodControlConfig) : null; const logFunction = console.log; const writer = createWriter(formatter, logFunction, writerOptions); const write = (level, message, data) => { if (logLevel.value < level.value) { return; } const check = floodControl ? floodControl.check(message, data) : "log"; const payload = { message, data }; switch (check) { case "log": writer.write(level, coordinates, payload); break; case "suppress": if (floodControl && floodControl.getSuppressedCount(message, data) === 1) { const originalLevel = level; const newPayload = { message: `Started suppressing repeated log message`, data: [] }; writer.write(originalLevel, coordinates, newPayload); } break; case "resume": { const count = floodControl ? floodControl.getSuppressedCount(message, data) : 0; const resumePayload = { message: `Stopped suppressing repeated log message. Suppressed ${count} times.`, data: [] }; writer.write(level, coordinates, resumePayload); writer.write(level, coordinates, payload); break; } } }; const startTimeLogger = (logLevel2, coordinates2, payload) => { const timerMessage = formatter.timerMessage(logLevel2, coordinates2, payload); logLevel2.value >= DEBUG.value && console.time(timerMessage); return { end: () => { logLevel2.value >= DEBUG.value && console.timeEnd(timerMessage); }, log: (...data) => { logLevel2.value >= DEBUG.value && console.timeLog(timerMessage, ...data); } }; }; return { emergency: (message, ...data) => { write(EMERGENCY, message, data); }, alert: (message, ...data) => { write(ALERT, message, data); }, critical: (message, ...data) => { write(CRITICAL, message, data); }, error: (message, ...data) => { write(ERROR, message, data); }, warning: (message, ...data) => { write(WARNING, message, data); }, notice: (message, ...data) => { write(NOTICE, message, data); }, info: (message, ...data) => { write(INFO, message, data); }, debug: (message, ...data) => { write(DEBUG, message, data); }, trace: (message, ...data) => { write(TRACE, message, data); }, default: (message, ...data) => { write(DEFAULT, message, data); }, time: (message, ...data) => { const payload = { message, data }; return startTimeLogger(logLevel, coordinates, payload); }, get: (...additionalComponents) => { return createLogger(logFormat, logLevel, { category: coordinates.category, components: [...coordinates.components, ...additionalComponents] }, floodControlConfig, writerOptions); }, destroy: () => { if (floodControl) { floodControl.destroy(); } } }; }; // src/logging.ts var getLogger = (name) => { const config = configureLogging(); const logger = createBaseLogger(name, config); return logger; }; var createBaseLogger = (name, config) => { let { logLevel } = config; const { logFormat, floodControl } = config; const overrides = config.overrides; if (overrides && overrides[name]) { logLevel = overrides[name].logLevel; } const coordinates = { category: name, components: [] }; return createLogger(logFormat, logLevel, coordinates, floodControl); }; // src/middleware/maskMiddleware.ts function maskLogEntry(entry, config = defaultMaskingConfig) { if (!config.enabled) { return entry; } const maskedEntry = { ...entry }; if (typeof maskedEntry.message === "string") { maskedEntry.message = maskWithConfig(maskedEntry.message, config); } if (maskedEntry.data !== void 0) { maskedEntry.data = maskWithConfig(maskedEntry.data, config); } if (maskedEntry.meta !== void 0) { maskedEntry.meta = maskWithConfig(maskedEntry.meta, config); } for (const [key, value] of Object.entries(maskedEntry)) { if (typeof value === "string" && key !== "level" && key !== "timestamp") { maskedEntry[key] = maskWithConfig(value, config); } } return maskedEntry; } function createMaskingMiddleware(config = defaultMaskingConfig) { return (entry) => maskLogEntry(entry, config); } function maskLogEntries(entries, config = defaultMaskingConfig) { if (!config.enabled) { return entries; } return entries.map((entry) => maskLogEntry(entry, config)); } // src/index.ts var index_default = { getLogger }; export { LogFormat_exports as LogFormat, LogLevel_exports as LogLevel, createMaskingMiddleware, index_default as default, defaultMaskingConfig, getLogger, maskLogEntries, maskLogEntry, maskObject, maskString, maskWithConfig, safeFormat, safeInspect, stringifyJSON };