Files
ripster/backend/src/services/logger.js
T
2026-06-08 12:15:36 +02:00

240 lines
5.5 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { logLevel } = require('../config');
const { getBackendLogDir, getFallbackLogRootDir } = require('./logPathService');
const { getServerTimestamp } = require('../utils/serverTime');
const LEVELS = {
debug: 10,
info: 20,
warn: 30,
error: 40
};
const ACTIVE_LEVEL = LEVELS[String(logLevel || 'info').toLowerCase()] || LEVELS.info;
let consoleConfig = {
enabled: true,
levels: {
debug: true,
info: true,
warn: true,
error: true
},
http: true
};
function normalizeBoolean(value, fallback = true) {
if (value === null || value === undefined) {
return fallback;
}
return Boolean(value);
}
function ensureLogDir(logDirPath) {
try {
fs.mkdirSync(logDirPath, { recursive: true });
return true;
} catch (_error) {
return false;
}
}
function resolveWritableBackendLogDir() {
const preferred = getBackendLogDir();
if (ensureLogDir(preferred)) {
return preferred;
}
const fallback = path.join(getFallbackLogRootDir(), 'backend');
if (fallback !== preferred && ensureLogDir(fallback)) {
return fallback;
}
return null;
}
function getDailyFileName() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `backend-${y}-${m}-${day}.log`;
}
function safeJson(value) {
try {
return JSON.stringify(value);
} catch (error) {
return JSON.stringify({ serializationError: error.message });
}
}
function truncateString(value, maxLen = 3000) {
const str = String(value);
if (str.length <= maxLen) {
return str;
}
return `${str.slice(0, maxLen)}...[truncated ${str.length - maxLen} chars]`;
}
function sanitizeMeta(meta) {
if (!meta || typeof meta !== 'object') {
return meta;
}
const out = Array.isArray(meta) ? [] : {};
for (const [key, val] of Object.entries(meta)) {
if (val instanceof Error) {
out[key] = {
name: val.name,
message: val.message,
stack: val.stack
};
continue;
}
if (typeof val === 'string') {
out[key] = truncateString(val, 5000);
continue;
}
out[key] = val;
}
return out;
}
function writeLine(line) {
const backendLogDir = resolveWritableBackendLogDir();
if (!backendLogDir) {
return;
}
const daily = path.join(backendLogDir, getDailyFileName());
const latest = path.join(backendLogDir, 'backend-latest.log');
fs.appendFile(daily, `${line}\n`, (_error) => null);
fs.appendFile(latest, `${line}\n`, (_error) => null);
}
function emit(level, scope, message, meta = null) {
const normLevel = String(level || 'info').toLowerCase();
const lvl = LEVELS[normLevel] || LEVELS.info;
if (lvl < ACTIVE_LEVEL) {
return;
}
const timestamp = getServerTimestamp();
const payload = {
timestamp,
level: normLevel,
scope,
message,
meta: sanitizeMeta(meta)
};
const line = safeJson(payload);
writeLine(line);
if (!shouldEmitToConsole(normLevel, scope)) {
return;
}
const print = `[${timestamp}] [${normLevel.toUpperCase()}] [${scope}] ${message}`;
if (normLevel === 'error') {
console.error(print, payload.meta ? payload.meta : '');
} else if (normLevel === 'warn') {
console.warn(print, payload.meta ? payload.meta : '');
} else {
console.log(print, payload.meta ? payload.meta : '');
}
}
function shouldEmitToConsole(level, scope) {
if (!consoleConfig.enabled) {
return false;
}
const normalizedLevel = String(level || 'info').toLowerCase();
if (consoleConfig.levels[normalizedLevel] === false) {
return false;
}
const normalizedScope = String(scope || '').trim().toUpperCase();
if (normalizedScope === 'HTTP' && !consoleConfig.http) {
return false;
}
return true;
}
function child(scope) {
return {
debug(message, meta) {
emit('debug', scope, message, meta);
},
info(message, meta) {
emit('info', scope, message, meta);
},
warn(message, meta) {
emit('warn', scope, message, meta);
},
error(message, meta) {
emit('error', scope, message, meta);
}
};
}
function setConsoleOutputEnabled(enabled) {
consoleConfig.enabled = Boolean(enabled);
return consoleConfig.enabled;
}
function isConsoleOutputEnabled() {
return consoleConfig.enabled;
}
function configureConsoleOutput(options = {}) {
const opts = options && typeof options === 'object' ? options : {};
if (Object.prototype.hasOwnProperty.call(opts, 'enabled')) {
consoleConfig.enabled = normalizeBoolean(opts.enabled, true);
}
if (opts.levels && typeof opts.levels === 'object') {
const sourceLevels = opts.levels;
for (const level of Object.keys(LEVELS)) {
if (Object.prototype.hasOwnProperty.call(sourceLevels, level)) {
consoleConfig.levels[level] = normalizeBoolean(sourceLevels[level], true);
}
}
}
if (Object.prototype.hasOwnProperty.call(opts, 'http')) {
consoleConfig.http = normalizeBoolean(opts.http, true);
}
return getConsoleOutputConfig();
}
function getConsoleOutputConfig() {
return {
enabled: Boolean(consoleConfig.enabled),
levels: {
debug: Boolean(consoleConfig.levels.debug),
info: Boolean(consoleConfig.levels.info),
warn: Boolean(consoleConfig.levels.warn),
error: Boolean(consoleConfig.levels.error)
},
http: Boolean(consoleConfig.http)
};
}
module.exports = {
child,
emit,
setConsoleOutputEnabled,
isConsoleOutputEnabled,
configureConsoleOutput,
getConsoleOutputConfig
};