@fjell/logging
Version:
Logging for Fjell
731 lines (718 loc) • 21.3 kB
JavaScript
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
};